Dans notre outil, nous utilisons le langage twig. Cela permet une grande flexibilité, et d'avoir un moteur déjà bien robuste.

Prenons par exemple l'affichage d'une date avec ce code :

Post.Date|datel(Lang.Get("Default date format"))

Notre outil étant mutli langue, et les formats de date changeant avec la langue, nous utilisons une locale. En pratique, il s'agit d'une chaîne de caractères indiquant le format.

On pourrait avoir HH:mm pour l'heure par exemple.

Dans l'analyse du bug du jour, nous avons un signalement de date fausse sur certaines pages. Après vérifications nous trouvons une page datant du 3 janvier 2010, mais qui est affichée comme étant le 3 janvier 2009.

L'enquête se poursuit en vérifiant les dates des autres pages : la quasi totalité des pages affichent une date juste. On finit par regrouper les pages à problèmes suivant le critères : certaines pages de début janvier de certaines années (mais pas toutes).

Le collègue qui cherchait a trouvé le problème.

Default date format = MMMM d Y

Dans la documentation de datel, Y = year of "Week of Year". Il s'agit de l'année durant laquelle le week end du jour en question a eu lieu.

Il s'agit donc d'un bug visible pour les premiers jours de janvier pour lesquels la semaine commençait l'année d'avant. Tordu non ?

Pour la petite histoire, le code corrigé sans bug donne :

Default date format = MMMM d yyyy

Lorsqu'on teste avec jenkins, cucumber et watir, on obtient des rapports détaillés, qu'il faut lire et analyser en permanence.

Depuis plusieurs mois, nous avons dans nos locaux une télé dédiée à l'affichage de l'état général de l'ensemble de nos tests. Pour cela, le plugin Jenkins Wall Display nous a suffit dans un premier temps. Nous affichons sur la télé une page web qui fait défiler

Au total, sur ces écrans, nous affichons le résumé :

  • Plus de 1 300 test php unit (et leur 5 000 assertions)
  • Près de 500 tests unitaires JS
  • Plus de 950 scénarios cucumber pour les tests fonctionnels.

En outre, nous affichons aussi quelques graphiques sensibles de l'état de la production : nombre de compte crées, etc.. On a beau regrouper toutes ces informations, il arrive un moment où l'affichage n'est plus lisible.

Exemple avec les tests fonctionnels

Le constat est moins alarmant pour les tests unitaires. Les chiffres exposent bien pourquoi :

  • Tests fonctionnels : 40 jobs jenkins * 3 navigateurs
  • Tests unitaires : 18 jobs jenkins * 2 environnement

Comment afficher proprement les résultats

Je suis parti en quête d'une solution pour afficher tous nos résultats proprement, avec quelques contraintes :

  • Je préférais afficher tous les résultats sur un seul écran par type de tests
  • Je veux pouvoir afficher un résumé ( x tests KO / x tests total)
  • Je veux pouvoir séparer facilement les affichages d'un même type de tests (par navigateur / environnement)

Ma quête fut longue et semée d'embûches :

  • Tentative d'utilisation de l'API, mais c'était déjà trop lent alors que je ne calculais / affichais pas la moitié de ce que je voulais (45 secondes environ)
  • Plusieurs plugin jenkins, mais aucun ne permettant ce que je veux
  • Un Plugin jenkins faisant la moitié de ce que je voulais, mais complètement bugué

Mon premier plugin jenkins

Je suis donc parti à l'assaut de la documentation de jenkins pour coder un plugin fait-maison qui ferait exactement ce que je voudrais.

Ce que j'ai bien aimé dans cette création :

  • Tout en java. Je n'avais pas codé en java depuis 10 ans je pense, et j'ai perdu, mais vite repris mes repères.
  • Bosser sur un plugin que je pourrais diffuser en open-source à la fin :)
  • La possibilité de faire exactement ce que je veux, sans être limité par un plugin déjà fait.

Ce que je n'ai pas aimé :

  • La documentation : elle existe, elle semble complète. Mais je trouve l'organisation moyennement bonne. J'ai plusieurs fois été à la recherche d'exemple de code pour trouver la fonction dont j'avais besoin, que je ne trouvais pas dans la doc.
  • L'installation de maven qui marche du premier coup sur un de mes postes, mais se passe mal sur l'autre.

Au final, ça donne quoi ?

J'ai donc un plugin qui permet de créer une nouvelle "View". On peut y configurer :

  • Les noms des colonnes, et la façon de filtrer les jobs dans chaque colonne, par des expression régulières.
  • Affichage dans chaque colonne du temps pris pour ces tests, du nombre de scénarios en échec et du nombre total de scénarios. (Afficher le nombre de jobs est beaucoup plus simple, mais donne beaucoup moins d'informations : un job en échec pour 1 / 34 est moins grave qu'un test avec 4 / 4 erreurs)
  • Affichage de la liste des jobs en échec (avec le nombre de scénarios)
  • Une colonne à part, qui affiche le temps total sur jenkins (master + slave)
  • Une liste de jobs spéciaux (dans mon cas : ceux qui lancent les autres, la génération de la doc...)

Le plugin est sur github : https://github.com/fabrice31/CucumberJenkins

Et maintenant ?

J'ai configuré le plugin pour fonctionner avec cucumber, puisque je suis au coeur des tests fonctionnels. Il faut maintenant que je me débrouille pour que cet affichage fonctionne aussi bien avec tests php unit (séparés par des regexp par environnement cette fois)

Affichage de résultats cucumber Plugin jenkins affichage de résultats cucumber

Lors de tests, il arrive souvent qu'on ait besoin de "faire le ménage" pour retrouver un état comme à l'origine. Plusieurs stratégies possibles, en fonction des technologies qu'on souhaite utiliser.

Un par un, à la fin

Après chaque scénario, supprimer ce qui a été ajouté, annuler les modifications une par une.

Avantages :

  • Facile à mettre en place (on réutilise ce qui a déjà été fait)

Défauts :

  • Si le scénario plante, on ne remet pas en l'état
  • Chaque scénario prend un peu plus de temps (jusqu'au double, dans le pire des cas)

Un par un, au début

Avant chaque scénario, remettre les valeurs par défauts pour le test

Avantages :

  • Facile à mettre en place (on réutilise ce qui a déjà été fait)

Défauts :

  • Si la remise à zéro ne marche pas, on ne teste pas
  • Chaque scénario prend un peu plus de temps (jusqu'au double, dans le pire des cas)

Tous d'un coup, dans une routine à part

Puisqu'on utilise un outil comme jenkins pour organiser tous les tests, on peut ajouter une routine "à part", qui va se connecter sur chaque compte pour effectuer cette remise à zéro.

Avantages :

  • Toutes les procédures de remises à zéro sont au même endroit

Défauts :

  • Un peu plus long à mettre en place
  • Demande d'ajouter une sécurité pour ne pas la lancer au mauvais moment
  • Ca prend du temps (mais autant que de le faire un par un)

Optimisation : utilisation d'une API

Dans mon cas, nous disposons aussi d'une API publique (en alpha, elle n'est pas encore ouverte à tout le monde)

Avantages :

  • Pas d'interface web, cela devrait aller plus vite
  • Ajoutera "en passant" des tests sur la public API

Défauts :

  • Je ne maitrise pas du tout Oauth 1.0a (ça me permettra d'apprendre : tant mieux)
  • Nous disposons d'un SDK pour faciliter l'utilisation de l'API, en php. Pour des raisons pratiques, je préfère le faire en ruby, le serveur de tests étant déjà configuré pour cela. Je pars donc "de zéro"

Utiliser une API OAuth 1.0a en Ruby

On trouve un peu de documentation pour le faire, et quelques exemples. Quelques remarques sur ces articles : * La quasi totalité expose le même code d'exemple. * Rares sont ceux qui signalent la version d'OAuth * Les exemples se concentrent beaucoup sur "comment se connecter à l'api", et très rarement sur "comment l'utiliser"

Voilà quelques astuces pour utiliser "rapidement" une API OAuth en ruby

Utilisation de gems

Elles facilitent la vie, autant en profiter.

require 'oauth'
require 'watir-webdriver'
require 'json'

Définition de constantes de l'api

OB_API_SERVER                 = 'http://url_server_api'
OB_API_BASE_URL               = OB_API_SERVER+'/public/0.1'
OB_API_ENDPOINT_REQUEST_TOKEN = '/oauth/request_token'
OB_API_ENDPOINT_ACCESS_TOKEN  = '/oauth/access_token'
OB_API_ENDPOINT_AUTHORIZE     = '/oauth/authorize'
OB_API_MY_CONSUMER_KEY        = '######'
OB_API_MY_CONSUMER_SECRET     = '######'

Obtenir la connexion à l'API

# create the OAuth consumer
@consumer = OAuth::Consumer.new( 
    OB_API_MY_CONSUMER_KEY,
    OB_API_MY_CONSUMER_SECRET,
    {
        :oauth_version      => '1.0a',
        :site               => OB_API_SERVER,
        :request_token_path => OB_API_ENDPOINT_REQUEST_TOKEN,
        :access_token_path  => OB_API_ENDPOINT_ACCESS_TOKEN,
        :authorize_path     => OB_API_ENDPOINT_AUTHORIZE
    }
)
# show all debugs in console
# @consumer.http.set_debug_output($stdout)
# get the authorize url
@request_token = @consumer.get_request_token
# Make as if the user authorize the app
# If you make a web app, you should show the link to your user, and let it click it
# browse the authorize url
@browser = Watir::Browser.new
@browser.goto(@request_token.authorize_url())
# connect with account
@browser.text_field(:name => 'email').set("test@test.fr")
@browser.text_field(:name => 'passwd').set("monmotdepasse")
@browser.button(:name => 'loginSubmit').click
# give access
@browser.form.submit
# get verifier token
# browser url contain oauth_verifier_token
oauth_verifier_token = @browser.url.split("oauth_verifier=")[1]
# grant total access
@access_token = @request_token.get_access_token(:oauth_verifier => oauth_verifier_token)
# no more browser needed
@browser.close

Exemples d'utilisation de l'API OAuth

Une fois qu'on est connecté à l'API, on peut l'utiliser. Il faut lire sa documentation, en général assez touffue, pour savoir ce qu'on peut faire (lister / ajouter / modifier / supprimer). Voici deux exemples "simplifiés" pour mon exemple.

Classique

Afficher les informations sur le blog courant.

get_url_info =  OB_API_BASE_URL+'/blog/info'
current_blog_info = @access_token.get(get_url_info)
Avec une option

Afficher la liste des titres des articles en brouillons.

get_lists_content = OB_API_BASE_URL+'/blog/posts/draft?limit=20'
content_list = @access_token.get(get_lists_content)
# read json results
result = JSON.parse(content_list.body)
result['response'].each do | content |
    puts content['title']
end

Conclusions

  • Ma routine journalière prenait environ 55 minutes. La nouvelle routine, via l'api, dure environ 90 secondes.
  • J'ai depuis très envie de l'étendre pour ajouter de nouvelles procédures de remises à zéro.
  • La documentation est primordiale (que ce soit celle de l'API ou celle de OAuth)

Exécuter une partie des tests

Les scénarios sont taggués pour être lancé par jenkins par la commande : cucumber --tag @montag Cela permet de ne lancer qu'une partie des tests. (voir "Tester sur plusieurs navigateurs" pour les détails)

Serveurs

Côté serveur, nous avons 2 serveurs jenkins. Le principal, une grosse machine debian quadri core, qui exécute jusqu'à 3 jobs en même temps.

Ensuite, un serveur Windows, avec un jenkins "slave". L'esclave reçoit ses ordres du maître, qui lui demande d’exécuter des jobs particuliers. Il est sous Windows pour pouvoir les exécuter sur Internet Explorer.

Pour éviter les effets de bords entre les tests sur IE, pas de solution. On vide donc les cookies, le cache, etc à la fermeture d'Internet Explorer. On ne peut donc pas exécuter les tests pour IE en parallèle sur le même serveur : les sessions se mélangeraient.

Quelques chiffres

Les scenarios sont regroupés en feature, dont le temps d'execution est de maximum 10 minutes. Dans Jenkins on a 38 "jobs" sur FF (+ 34 sur IE)

Firefox

Chaque jour : 323 scénarios / 2693 step => 3h

IE (1 fois par semaine actuellement, 1 fois par jour bientôt)

Chaque jour : 302 scénarios / 2527 step => 4h 30

Cela représente 227 500 scénarios par an (pour 2 737 heures de tests).

Sans oublier les lancements à la demande de certains jobs.

Comptes utilisateurs

Du côté des scénarios, ils utilisent pour le moment 70 comptes utilisateurs différents (avec des options particulières, etc...)

Un changement prochain de notre offre me fait penser que les tests auront besoin de 100 comptes d'ici 2 à 3 semaines.

Sans oublier de dupliquer tout ça pour nos 2 environnements (test / stable)

Ni d'ajouter l'exécution de ces tests sur chrome.

Et on ajoute des tests fonctionnels en dehors de ce schéma, pour tester des choses particulière, que ce soit en production sur des points essentiels ou la validité de données particulières.

Et vous, combien vous testez ?

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)