Si vous avez plusieurs tests qui s’exécutent en même temps sur le même environnement (le serveur de test), il faut veiller à bien tester la "bonne chose".

Prenons un exemple de deux tests exécutés en même temps :

@scenario1
Scenario: Test modif pseudo
   Given I connect to my "pseudo" account
     When I change my nickname for "pseudo-tmp"
     When I publish an article "test pseudo-tmp"
     Then my published pseudo is "pseudo-tmp"
@scenario2
Scenario: Pseudo cannot be empty when you publish
   Given I connect to my "pseudo" account
     When I change my nickname for ""
     When I publish an article "test pseudo"
     Then my published pseudo is "pseudo"

Si les 2 scénarios se terminent "au même moment", nous aurons sur la page de contrôle 2 articles. L'un signé avec "pseudo-tmp", et l'autre avec "pseudo".

Si la vérification vérifie dans tout le contenu de la page des publications et pas seulement le dernier élément, cela pourrait bien se passer.

A un détail près :

  • A 10h, le test s’exécute, il fonctionne.
  • A 15h, le test s’exécute mais les articles parus à 10h sont peut-être toujours affichés. On aura donc bien un article "test pseudo" sur la page alors qu'un bug sera présent.

Ma solution : ajouter de l'aléatoire dans les tests. Avant chaque scénario, je crée un token, composé de : * 2 lettres du navigateur (ff, ie, ch..) * un nombre aléatoire. Et je l'affiche en console (bien pratique pour débugguer)

Before do
  $token = Token.new
  puts "Token for this test : #{$token.value}"
  /* je vous fais grace du code pour lancer le navigateur, ... */ 
End

Ma classe Token :

# Use to got random value
# Useful to post a data with random string but keep the value to check it later
# Author:: Fabrice
class Token
  # Create a new token
  # Author:: Fabrice
  def initialize
    @value = "#{ENV['BROWSER']}#{256*256+rand(1024*1024)}"
  end
  # Get the value of the last token
  # Author:: Fabrice
  def value
    @value
  end
end

Ensuite, sans modifier le scénario, on modifie le code des étapes. Lorsqu'on sauve une donnée, on ajoute le token "courant" dans le champ.

title = "#{title} #{$token.value}"

Et côté vérification, on en tient compte également.

def open_article(title, token = false)
  if(token)
    @browser.link(:text => "#{title} #{$token.value}").click
  else
    @browser.link(:text => "#{title}").click
  end
end

Parfois, on a besoin de pouvoir jouer "sans token" pour des points bien précis : on prévoit alors un paramètre pour s'en passer. (Exemple, pour la saisie d'un email)

Ce genre de solution peut aussi vous éviter des effets de bords si vous publiez sur le même compte des articles avec le même titre. (Ceci, c'est un autre test à part entière)

Il y a plusieurs mois, j'avais tweeté "Les bugs, ça coûte cher : Noël sans solde pour des milliers de soldats français"

Source : http://www.lemonde.fr/international/article/2012/12/21/des-milliers-de-soldats-francais-non-payes-a-cause-d-un-bug-informatique18096063210.html

Pour résumer : dans les anciens logiciels de paie des militaires français, il y avait parfois des petites erreurs, des petits bugs, qui étaient corrigés les mois suivants. Mais en 2012, mise à jour complète du logiciel.

Louvois, le logiciel le plus buggué de l'histoire ?

La dernière version du logiciel, surnommée "Louvois" (Logiciel unique à vocation interarmées de la solde) , est tellement remplie de bugs que certains militaires ont de légers problèmes. (Exemples en fin d'article)

Il y a un mois, on pouvait lire dans la presse que les bugs devaient être corrigés pour Noël 2012, mais ne l'étaient pas encore mi février 2013.

Source : http://www.lemagit.fr/technologie/applications/applications-transversales-pgi/2013/02/20/logiciel-de-paie-louvois-la-guerre-de-le-drian-est-loin-detre-gagnee/

On envisagerait donc de revenir à l'ancien logiciel, moins buggué. Mais cela prendrait entre 12 et 14 mois, donc, mi 2014 au mieux.

Une autre source indique que l'armée de l'air paierait déjà ses hommes via un autre logiciel pour éviter les bugs : http://www.marianne.net/blogsecretdefense/Louvois-l-armee-de-Terre-pourrait-revenir-a-l-ancien-systeme-de-paiement-des-soldes_a942.html

Les erreurs occasionnées par Louvois représentent 1 % de la masse salariale du ministère de la Défense, pour près de 120 000 incidents Le Canard Enchaîné

Et pendant ce temps, dans l'armée de terrain

  • Saisie sur salaire à un miliaire, pour une somme déjà payée au trésor public par le militaire par ailleurs. (Donc, il est dans le rouge, et paie 2 fois). Temps avant remboursement : plusieurs mois.
  • Un homme qui doit faire vivre sa famille un mois entier avec 60 euros : http://storify.com/fabrice31/l-armee-mauvais-payeur
  • Soldes payées avec des mois de retard sur le terrain
  • Primes prévues mais reportées, payées avec plusieurs mois de retard...
  • Sur une compagnie de 120 hommes, pour le même mois, déjà plus de 20 cas recensés avec une erreur sur le solde, de 150 à 1 400 €. (La cause serait la prime versée le mois précédent, considérée comme un trop perçu par le logiciel)

Si des militaires veulent poursuivre la liste, n'hésitez pas, je compléterai avec plaisir cette liste de bugs. (un formulaire de contact anonyme est disponible en bas de page)

L'armée ne paie pas toujours ses militaires.

Et si c'était vous ?

Rappel : les militaires n'ont pas le droit de manifester / faire grève...

Imaginez, si cela ne touchait pas l'armée, mais une société lambda ?

  • Une société privée avec un bug aussi important (même une seule fois) qui ne corrige pas dans les jours qui suivent la situation de tous les salariés : que diront les prud'hommes ?
  • Une société qui a un bug dans 1% des paies qu'elle verse, Sur une petite PME de 20 personnes, cela fait 1 seule erreur de paie tous les 5 mois. 2.5 erreurs par an.
  • Pour l'armée, ça représente 3220 personnes par mois. (322 000 emplois en 2007)
  • Imaginez, si ce mois-ci, votre conjoint(e) reçoit un salaire de 60 € à cause d'une erreur de logiciel, et la suite viendra "plus tard", délai non confirmé. De quoi vous passez vous ? Payer votre loyer, vos crédits ? Les couches de bébé ?

Une page facebook parlait en 2012 du problème : https://www.facebook.com/pages/Un-paquet-de-Gauloises/416293095105059

On peut être un grand nom de la vente en ligne, mais avoir malgré cela des problèmes sur son site web.

Parfois, cela se passe "presque bien" : tout le monde se souvient encore de l'homme nu de La Redoute (une photo présentant un produit avec en fond un homme nu). Tout le monde a rigolé, s'est moqué d'eux. Ils ont corrigé le tir, puis ont lancé un jeu pour trouver certains objets dans d'autres images pour les gagner.

Et parfois, c'est le "drame"

Prenons un cas pratique, qui m'est arrivé.

Je suis client chez http://www.boulanger.fr/ depuis longtemps, j'ai une carte de fidelité, etc. En janvier, j'ai rencontré des difficultés pour utiliser un chèque cadeau sur une commande. J'ai contacté leur support par un formulaire par mail : aucune nouvelle.

J'ai tweeté : une cm m'a demandé une copie du mail et m'a confirmé avoir transférée ma demande à l'équipe technique. Au menu : des problèmes d'ergonomie, de textes pas clairs...

Ce week-end, j'ai reçu un mail pour me dire que mon chèque cadeau n'était pas utilisé. Petit tour sur mon espace client, quelques soucis rencontrés en quelques minutes.

Les commandes fantômes

Page "Commandes en cours", j'ai deux commandes notées "expédiées" :

  • Celle de fin novembre. Que j'ai reçue en décembre, sans souci.
  • Celle de janvier. Que j'ai reçue (en deux fois, l'un des articles était indisponible. J'ai été averti de l'indisponibilité, j'ai choisi d'attendre : 5 semaines avant la livraison)

La saisie impossible (d'un chèque cadeau)

Pour saisir un chèque cadeau, quelques étapes :

  • Validez votre panier
  • Au milieu de la page de paiement, cochez oui à la phrase "Vous béneficiez de cartes cadeau, cartes satisfaction ou chèques fidélité ?"
  • Sélectionnez "Chèque cadeau".
  • Pour les férus d'accessibilité : c'est un div. (L'image de fond est appliqué au div parent)
  • Pour les férus de webperf : l'image est chargée 2 fois, avec 2 adresses différentes.
  • Saisissez le numéro du chèque (copier / coller, en faisant attention aux espaces)
  • Saisissez le montant du chèque (Apparement, Boulanger ne connait pas le montant en fonction du numéro de chèque...) Attention, pas de format indiqué : 25,10 / 25.10 / 25€10 : bonne chance.
  • Recopiez un captcha
  • Validez le chèque

8 moyens de paiements. +1 caché

Voilà, la page se recharge, referme la partie chèque, et vous passez à la suite. Oui, mais :

  • Si tout va bien, la ligne pour le chèque figure en bas de la page d'après ce qu'ils disent.
  • Si une erreur s'est produite, vous devez :
    • Au milieu de la page de paiement, il faut cocher à nouveau la phrase "Vous béneficiez de cartes cadeau, cartes satisfaction ou chèques fidélité ?"
    • Sélectionner "Chèque cadeau".
    • Regarder si par hasard, une ligne rouge n'est pas affichée. Avec un message d'erreur du genre "Le paiement n'est pas valide". Bonne chance pour comprendre quelle est l'erreur. Si vous tapez le montant "AA" ou un numéro de chèque erroné, vous aurez la même erreur.

Un message d'erreur (enfin, façon de parler)

Vous avez demandé le service client, ne quittez pas

En bas de page, un lien contact : je me dis que je vais leur expliquer ce que je pense de leur site.

  • Je clique sur le lien en bas de page
  • Une page apparaît avec une liste de 8 "services" pour les contacter. Je clique sur "Contactez Boulanger.fr" (accessibilité : nulle. c'est une image sans texte alternatif)
  • Une page différente apparaît, avec les 8 services, où celui que j'ai sélectionné est "ouvert". Aucun des choix proposés ne me convient. Je change de service pour "Les produits ou les services (je vous ai parlé de la non accessibilité du site ?). Voilà, je peux choisir un type de contact qui me convient "Question concernant la carte de fidélité"
  • Et là, alors que je suis connecté, qu'ils ont donc mon adresse, mon numéro de client, mon email, je dois remplir quelques informations.
    • Informations obligatoires (présence de * à coté de l'intitulé) : Nom, prénom, numéro de téléphone (là encore, pas de format... je choisi donc le format international), email, choix du mode de contact de leur part (email ou téléphone), et ma question
    • Le numéro de la carte de fidélité n'est pas obligatoire : ouf.
    • Des cases pour accepter ou non de recevoir des mails de promos de Boulanger et de ses partenaires (avec une ligne en gris sur fond blanc, que j'ai trouvé peu lisible...)
  • J'écris un long mail (cet article est issu du mail). Enfin, je ne l'écris pas dans le champ : le textarea de 6 lignes, 32 colonnes, non redimensionnable est trop petit pour ma prose.
  • Je l'envoie. Ah tiens, j'ai un message d'erreur (il est clair cette fois) : je n'ai pas saisi mon numéro de carte boulanger. Même s'il est "logique" de devoir saisir le numéro sur la page de contact à ce sujet, c'est pourtant le seul champ que je ne suis pas obligé de remplir.
  • Je remplis le champ par mon numéro, qui d'après leur aide (qui apparaît au survol.. accessibilité...) doit faire 10 caractères. Sauf que je peux en mettre 12 ou plus en fait. Le champ n'est pas limité en nombre de caractères.
  • J'arrive tout de même à envoyer mon mail, et j'ai donc un joli message de confirmation. Par expérience, j'aurais aimé recevoir une copie par mail. Pour ne pas perdre mon message, et l'avoir si on me demande de redonner des infos.

Boulanger, une fois le mail envoyé

Pour aller plus loin

Après avoir envoyé le mail, j'ai commencé mon article, et j'ai du coup été tatillon : lecture du code source de quelques pages, etc. Quelques trucs qui me chiffonnent en vrac :

  • La page de contact n'a pas de balise "title"
  • A la louche : 4 fichiers css (+2 pour le print), 20 fichiers js...
  • Le design du formulaire de contact en balise table dans une balise table dans une balise table.
  • La vérification des champs provoque un message d'erreur (un seul : c'est une bonne chose) mais est faite en javascript (accessibilité...)
  • Des balises mises en commentaires HTML.
  • La liste des mot-clés (meta keyword) me semble un peu longue, mais admettons. Par contre, le dernier mot-clé est terminé par un point. Étrange.

A la base, je comptais juste faire un achat rapide. Je viens de passer la soirée à faire un audit qualité (bug / ergonomie / accessibilité / webperf) à boulanger. C'est cadeau : je n'ai fait que survoler. J'encourage Boulanger à le faire à fond (ou mieux, à le faire faire par des experts). (Evidemment, si j'ai une réponse, je la publierais ici)

Tester de façon automatique, c'est bien. Le faire sur plusieurs environnements, c'est mieux.

Dans mon cas, par défaut, mes tests sont lancés sur Firefox. Sur un serveur jenkins-ci (sur debian), c'est en effet le navigateur le plus facile à gérer. Ils sont exécutés une fois par jour, et aussi à la demande.

Certains scenarios testent la version mobile de nos sites. Il nous faut aussi tester nos sites pour différents navigateurs. Pour cela, les profils et les tags de cucumber sont très intéressants.

Tags cucumber

Au dessus de chaque scénario, il est possible de lister des tags. On peut ensuite exécuter tous les tests portant un tag particulier.

Cela permet de découper l'exécution de nombreux scénarios en plusieurs jobs sur jenkins, ce qui est plus confortable : si un test passe dans le rouge, il faut analyser un rapport pour 10 scénario au lieu de 300. Si on veut re-tester ce point précis, on mettra également moins de temps.

On exécute les scénarios avec la ligne "cucumber --tag @montag". Si le tag n'existe pas, le résultat affichera immédiatement 0 scenarios.

@admin @montag @ie
Scenario: test un tag
  When I test
  Then I tested

Cucumber.yml

Les profils de cucumber se gère dans un fichier au format yaml, qui peut gérer beaucoup de choses :

  • Ajouter automatiquement des paramètres de tags
  • Passer des variables à l'environnement de tests, qui peut ensuite les utiliser.

Exemple avec une partie de mon fichier :

<% common = "-r support -r features --tags ~@wip --tags ~@danger 
--color --format pretty --format html -o results.html --format junit -o junit" %>
# default : staging and firefox
default: <%= common %> BROWSER=firefox
danger: -r support -r features --tags ~@wip --color --format pretty BROWSER=firefox
ie: <%= common %> ENV=staging BROWSER=ie
ff: <%= common %> ENV=staging BROWSER=firefox
chrome: <%= common %> ENV=staging BROWSER=chrome
mobile: <%= common %> ENV=staging BROWSER=mobile

La première ligne définit des paramètres commun à la plupart des profils :

  • On impose de récupérer le dossier features (dans lequel se trouvent les scenarios) et le dosser support (qui contient le code qui "gère" le navigateur)
  • On exclut les tests portant le tag @wip (work in progress) et le tag @danger
  • On demande l'affichage des résultats complets (avec les étapes et la couleur)
  • On demande l'export des résultats au format html et au format junit

Lorsqu'on utilise un profil, on peut lancer pour un tag donné. Exemple : cucumber . --tag @test -p chrome

Viennent ensuite les profils :

  • -p default: lance les tests sur firefox
  • -p danger: autorise le lancement des tests taggués @danger
  • -p ie, ff, chrome, mobile: lance les tests sur un navigateur particulier

Différences entre les navigateurs

Passer le nom du navigateur ne suffit pas, il faut également l'utiliser dans le code pour travailler avec le navigateur voulu.

Dans notre cas, nous lancons le navigaeur avant chaque scénario pour éviter qu'un test ait un impact sur le suivant.

Voici le code :

Before do
  case ENV['BROWSER']
  when 'ie'
    @browser = Watir::Browser.new :ie
  when 'mobile'
    mobile_useragent = "Mozilla/5.0 (iPhone; CPU iPhone OS 5_1_1 like \
        Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 \
        Mobile/9B206 Safari/7534.48.3"
    profile = Selenium::WebDriver::Firefox::Profile.new
    profile['general.useragent.override'] = mobile_useragent
    @browser = Watir::Browser.new :firefox, :profile => profile
  when 'chrome'
    @browser = Watir::Browser.new :chrome
  else
    # Default case : firefox
    download_directory = "#{Dir.pwd}/downloads"
    download_directory.gsub!("/", "\\") \
        if Selenium::WebDriver::Platform.windows?
    profile = Selenium::WebDriver::Firefox::Profile.new
    profile['browser.download.folderList'] = 2
    profile['browser.download.dir'] = download_directory
    profile['browser.link.open_newwindow'] = 3
    profile['browser.helperApps.neverAsk.saveToDisk'] = \
        "application/octet-stream"
    profile['network.http.use-cache'] = false
    @browser = Watir::Browser.new :firefox, :profile => profile
  end
  # Resize window
  if ENV['BROWSER'] != 'mobile'
    @browser.window.resize_to(1100, 900)
  else
    @browser.window.resize_to(640, 960)
  end
end

On exploite le nom du navigateur pour lancer le bon, avec des paramètres particuliers.

  • Le mobile lance un Firefox avec le profil d'un navigateur Iphone (et une résolution de classique pour un mobile)
  • Pour internet explorer et chrome, il faut avoir téléchargé les exécutables (sur le groupe selenium) et les avoir ajoutés au path
  • Pour firefox, on ajoute des paramètres pour effectuer des tests d'upload et download de fichiers)

Les tests ont-ils changés?

Lorsque j'ai commencé à coder ces tests, il y a plus d'un an, j'avais anticipé l'architecture pour gérer ce genre de problématique. La mise en place n'a pourtant pas eu lieu sans douleur.

Certains navigateurs ne se comportent pas tout à fait comme les autres. Il faut donc mettre à jour son code pour qu'il soit le plus "ré utilisable possible".

Dans le cas d'Internet Explorer, les modifications apportées :

  • Ajout d'un tag @ie sur les tests compatibles (et stables) avec IE.
  • Dans certains cas de remplissage de formulaire, ajout d'une ligne de code pour donner le focus au champ à remplir
  • Modification des timeout du projet.

On lance donc les tests pour Internet Explorer avec la commande cucumber --tag @ie -p ie. On peut changer le tag, mais certains ne fonctionnent pas encore sous IE à cause de codes vraiment spécifiques.

Voici deux exemples de code modifé spécifiquement pour IE :

if ENV['BROWSER'] == 'ie'
  # internet explorer is slower : timeout increase
  SHORT_TIMEOUT = 5
  TIMEOUT = 15
  LONG_TIMEOUT = 25
else
  SHORT_TIMEOUT = 2
  TIMEOUT = 9
  LONG_TIMEOUT = 25
end

Ce code définit les constantes des timeout pour l'ensemble des test.

if ENV['BROWSER'] == 'ie'
  sleep(1) # need for IE purpose
end
self.wait_until(LONG_TIMEOUT){
  self.login_element.exists? and self.login_element.visible?
}
self.username_element.focus # need for IE purpose
self.username = @account.email

Les commentaires dans le code parlent d'eux mêmes. Documenter le code spécifique à IE est d'ailleurs indispensable si on veut garder ses modifications qui semblent "inutiles".

Si les tests avec Firefox sont joués chaque jour sur un serveur, en 2h45 environ, ceux pour IE sont exécutés en local, en 3h, alors que certains tests ne sont pas exécutés sur IE.

Ce sont donc plus de 300 scénarios par jour, 12 000 par mois. Sans compter les tests unitaires, les tests manuels...

Vroici l'histoire d'un test fonctionnel "basique" mais techniquement compliqué à mettre en place.

Passé

Depuis plusieurs mois, nous sommes 2 à developper les tests automatiques. Nous avons une longue liste de tests à ajouter que nous dépilons progressivement. Certains tests sont présents depuis longtemps dans cette liste, mais nous ne réussissons tout simplement pas à les créer, pour des raisons purement techniques. D'autres sont en attente de la correction du bug sur le produit.

Lundi

Pour l'un d'eux, je réussi finalement à trouver une solution tarabiscotée qui semble marcher. La solution ne me plait pas totalement, cela revient à exécuter du javascript dans un navigateur que je controle avec watir, lancé depuis un cucumber géré par un jenkins.

L'utilisation du javascript (langage que je ne maîtrise pas) pour ce détail me gêne : s'il me permet de tester la fonctionnalité, je ne pourrais pas simplement tester certains points de la fonctionnalité.

En outre, cela m'empêche d'être sur de moi pour ce test : j'ai peur qu'il ne soit pas stable et pérenne.

Mercredi

Mercredi dernier, j'ai remonté un problème qui entrainait un problème en cascade sur l'ensemble de mes tests fonctionnels (40 scenarios échoués sur 270).

Il y avait deux importants chantiers effectués par mes collègues pour des mises à jours importantes, qui demandait des corrections.

Il faut savoir se fier à ses tests

Jeudi

Jeudi, c'est normalement la fin du codage des nouveautés pour le sprint en cours. Le jeudi c'est donc débug, tests, préparation du changelog et de la mise en production...

Lorsque j'arrive un jeudi avec mes indicateurs qui clignotent en rouge, je n'aime pas ça. Je survole rapidement, vois que c'est un problème commun pour les 10 premiers rapports que j'analyse en profondeur. Je demande donc une nouvelle correction, puis je relance des tests qui semblent être juste des faux positifs. Certains reviennent à la normale.

Je désactive certains tests qui paraissent toujours instables. Parmi eux, les tests ajoutés le lundi : je ne les sentais pas en les codant, je n'ai pas cru en eux lorsqu'ils ont signalé un bug.

Lundi

Lundi, jour de la mise en production. Une fois finie, l'équipe teste en production les changements pour vérifier que tout est normal.

En faisant un test sur tout à fait autre chose, je me suis dit "y'a un truc qui cloche, (pas ce que je suis en train de tester), mais je vois pas quoi". Mon instinct me disait que j'avais raté un truc. Je fais donc un tour du côté du support.

Les premiers retours utilisateurs signalent un bug m'a fait sourciller : on signale un bug sur la fonction testée depuis peu. Je vérifie le dernier rapport de tests : ils détectaient le bug, et étaient parfaitement stables.

Conclusion

En n'ayant pas cru en la stabilité d'un test que j'ai codé moi-même, j'ai laissé passer un bug en production.

J'ai vu un faux positif plutôt qu'un bug réel, alors que j'expliquais ce risque il y a longtemps.

J'ai l'impression d'avoir échoué à garantir la qualité de notre produit. J'ai eu trop confiance dans mes collègues, et pas assez en moi.