Cucumber pour les Railers

Ce billet va décrire comment mettre en place des tests Cucumber pour Ruby on Rails. Il est grandement recommandé de lire l'article de Vincent Coste, et d'avoir au moins des connaissances de base en Rails. C'est un article sur Cucumber et non sur le TDD/BDD, ainsi la méthode pour développer n'est pas conforme à ces méthodes de développement.

Nous avons pris la même application que dans l'article précédent. En effet, elle se prête bien aux tests Cucumber. Il s'agit du calcul d'une facture de téléphonie mobile, avec gestion du hors forfait. Notre application est loin d'être parfaite, des anciens choix de conception la rendent "particulière", et il va falloir faire avec pour l'intégration de nos tests. De plus, nos testeurs sont assez exigeants et veulent pouvoir écrire de nombreux tests et en français en plus !

Notre application

Celle-ci compte 4 objets/tables :

  • un utilisateur, User, comporte 2 champs :
    • nom : le nom de l'utilisateur
    • forfait_id : un lien vers son forfait
  • un forfait, Forfait, comporte 4 champs :
    • montant : le prix du forfait en euros
    • temps_communication : le temps de communication du forfait en secondes
    • hors_forfait : le montant du hors forfait par minutes
  • une facture, Facture, comporte 1 champ :
    • user_id : un lien vers un utilisateur
  • une ligne dans une facture, LigneFacture, 3 champs :
    • libelle : un libéllé pour cette ligne
    • duree : la durée en secondes de la communication
    • facture_id : un lien vers la facture associée

La partie métier de notre application est la méthode 'total' d'une facture qui calcule ... le total de la facture en prenant en compte le hors forfait.

Pour vous faire gagner du temps, le code source de l'application est disponible ici. Attention, il y a une incompatibilité entre la gem test-unit en version > 2 et Cucumber, vérifiez bien d'avoir les bonnes versions de cette gem (gem --list test-unit, pour lister les versions).

  • Téléchargez les sources
  • Installez les gems nécessaire pour les tests (cucumber, rspec, rspec-rails, webrat)
sudo rake gems:install RAILS_ENV=cucumber
  • Faites la migration
rake db:migrate RAILS_ENV=cucumber
  • Un generate pour créer l'arborescence Cucumber (déjà fait sur les sources)
script/generate cucumber

Ce dernier generate va créer :

  • features/, un répertoire où l'on va mettre nos fichiers de définitions de tests Cucumber
  • features/step_definitions/, un répertoire où l'on va mettre nos fixtures, le bout de code qui va lier le code Rails à Cucumber
  • features/support/, un répertoire pour l'intégration de Cucumber avec Rails et Webrat (on verra un peu plus tard ce qu'est Webrat)
  • lib/tasks/cucumber.rake, une tâche Rake pour Cucumber

Notre premier test

Une fois notre application en place, nos testeurs écrivent leurs premiers tests, dans le fichier features/montant_facture.feature. Nous reprenons ici un des tests du précédent article.

Capture d’écran 2009-12-04 à 11.30.39

Ils sont marrants nos testeurs, mais dans notre base de données on a des secondes pour les temps de communication, pas des minutes et des heures ! Ce n'est pas grave on est en Ruby donc on va pouvoir faire des merveilles. Remarquez le # language: fr qui permet d'indiquer à Cucumber que le fichier sera en français. On aurait aussi pu lui spécifier lors de son lancement, avec l'option -l fr. D'ailleurs lançons Cucumber sur notre projet :

Capture d’écran 2009-12-04 à 11.06.33

On remarque que Cucumber est assez intelligent pour nous expliquer calmement quels steps ne sont pas encore implémentés, et mieux, il nous donne directement la ligne à copier/coller dans notre code pour les implémenter.

Créons donc notre fichier features/step_definitions/facture_steps.rb, qui va faire le lien entre les spécifications Cucumber et notre application Rails.

Dans un premier temps, nous définissions les classes que nous allons utiliser :

require 'spec/expectations'  # Pour RSpec
require 'cucumber/formatter/unicode' # Pour le support de l'UTF-8 pour Cucumber

Le reste du fichier est ce qui couple les spécifications Cucumber avec Ruby/Rails. Le principe est de se baser sur des expressions régulières, plus au moins complexes. On aura donc toujours le schéma :

MotClé /regexp avec des groupes (.+)/ do |catch_de_la_regexp|
  traitement/tests
end

Ainsi pour la définition d'un premier Soit :

# Ici on récupère :
# - le temps du forfait (temps), l'unité de temps pour celui-ci (format_temps)
# - le montant du forfait en euros (prix)
# - le hors forfait, la durée (hors) et son unité de temps (format_hors)
Soit /^un forfait de (.+)(w+) pour (.+)€/mois avec un hors forfait de (.+)€/(w+)$/ do |temps, format_temps, prix, hors, format_hors|
  @forfait = Forfait.new(:montant => prix.to_f, :temps_communication => temps, :hors_forfait => hors)

  @facture = Facture.new
  @user = User.new(:nom => 'Dummy')

  @user.forfait = @forfait
  @user.factures << @facture
  @user.save!
end

Souvenez-vous, on peut ajouter plusieurs Soit avec le mot clé Et, donc on définit un autre Soit pour l'ajout de communications dans notre facture :

# De même, on récupère la durée de la communication (durée) et son unité de temps (format_duree)
Soit /^que j'ai une communication de (.+) (w+)/ do |duree, format_duree|
  if format_duree == "minutes"
    duree = duree.to_i * 60
  end

  new_line = LigneFacture.new(:libelle => 'Dummy', :duree => duree)
  @facture.ligne_factures << new_line
  @facture.save!
end

On définit notre Lorsque qui ne fait rien :

Lorsque /je calcule le total de la facture$/ do
  #noop
end

Enfin, on arrive à notre vrai test, qui va comparer le résultat de notre calcul (@facture.total) avec ce que le testeur a indiqué (resultat). On profite du fait que l'on soit en Ruby pour utiliser RSpec (la méthode should).

Alors /le total de la facture doit être de (.+)€/ do |result|
  result.to_f.should == @facture.total
end

Nous pouvons donc enfin tester notre premier test Cucumber pour rails, on lance donc la commande :

Capture d’écran 2009-12-07 à 13.02.53

Notre test ne passe pas !

En effet, nous avons oublié de traiter les unités de temps pour la création du forfait, rajoutons donc les lignes suivantes au début de la définition de notre Soit :

if format_hors == "min"
    hors = hors.to_i #le hors forfait est en minutes
  end

  if format_temps == "h"
    temps = temps.to_i * 60 * 60
  end

Relançons notre test :

Capture d’écran 2009-12-04 à 11.07.45

Enfin il passe !

Plus de tests avec Webrat

Nos testeurs adorés ont décidé d'écrire des tests de navigation pour l'application. Nous n'avons pas repris de tests du précédent article car ils seraient trop complexes à mettre en place et donc moins parlants :

Capture d’écran 2009-12-04 à 11.31.08

Heureusement pour nous il existe Webrat. C'est un outil en Ruby qui permet de faire des tests de navigation de site Web. De plus, Cucumber est sympa avec nous et puisqu'il fournit quelques fixtures intégrant directement Webrat. Malheureusement, celles-ci n'ont pas été traduites dans la langue de Molière. Nous allons donc devoir les réécrire (cf. features/steps/webrat_steps.rb).

Ainsi dans notre fichier features/steps/webrat_fr_steps.rb nous définissions les tests suivants :

Soit /^(?:|que je|je) suis sur (.+)$/ do |page_name|
  visit path_to(page_name)
end

Soit /^(?:|je) vais sur la page (.+)$/ do |page_name|
  visit path_to(page_name)
end

Soit /^(?:|que je|je) clique sur le bouton "([^"]*)"$/ do |button|
  click_button(button)
end

Lorsque /^(?:|que je|je) clique sur le lien "([^"]*)"$/ do |link|
  click_link(link)
end

Soit /^(?:|I )follow "([^"]*)" within "([^"]*)"$/ do |link, parent|
  click_link_within(parent, link)
end

Lorsque /^(?:|que je|je) remplis "([^"]*)" avec "([^"]*)"$/ do |field, value|
  fill_in(field, :with => value)
end

Soit /^(?:|I )fill in "([^"]*)" for "([^"]*)"$/ do |value, field|
  fill_in(field, :with => value)
end

Alors /^(?:|je )devrais voir "([^"]*)"$/ do |text|
  assert_contain text
end

Et dans le fichier features/steps/user_steps.rb

Soit /^que je n'ai aucun utilisateur$/ do
  User.delete_all
end

Alors /^je devrais avoir (d+) utilisateurs?$/ do |count|
  User.count.should == count.to_i
end

Capture d’écran 2009-12-04 à 11.09.52

Et c'est fini

Intégrer des tests Cucumber à Rails est très simple. Les fixtures pré intégrées permettent de faire des tests de navigation très rapidement. Malheureusement, il faut encore écrire les fichiers fixtures avec un éditeur de texte classique et les intégrer directement dans le code source de l'application. Espérons, qu'il y aura bientôt un plug-in pour Rails qui permettra aux testeurs d'écrire leurs tests directement depuis une interface Web.

Annexes

Vous pouvez trouver ici le fichier step pour Webrat en français. Et ici, le code source de l'application exemple.