Blog.

Maîtriser les tests unitaires dans Ruby on Rails

Maintenant que vous maîtrisez les fixtures (et que vous les avez écrit et testés), vous savez que votre réseau de relations entre les modèles ne posera pas de problèmes.

La tâche va consister dès lors à écrire plus précisément vos modèles, et en particulier vos validations. Bien entendu, nous allons commencer par écrire les tests :]

Introduction à Test:Unit

Test::Unit est en fait un framework de test intégré à la library standard de ruby. Pour mieux le comprendre, nous allons laisser Rails de côté quelques instants.

Test::Unit mêle quatre notions : les assertions, les testcases, les testsuites et les fixtures.

Une assertion est l’unité la plus élémentaire. Il s’agit d’une expression effectuant un test. Si le test est passé, rien de particulier ne se passe ; si le test échoue, une erreur sera affichée. La méthode de base est assert. Elle évalue si l’expression passée est vraie ou fausse. Vous pouvez spécifier un message personnalisé au cas ou le test échoue.

assert true
assert 1 < 2
assert false, "évidemment que non"

Les deux premières assertions passeront, la dernière échouera. Il existe tout un jeu de méthodes d’assertions intéressantes, comme assert_equal, assert_match, assert_not_same, assert_not_nil, assert_nothing_raised, etc. Je vous laisse regarder la doc officielle pour en faire le tour.

Un testcase est une classe dérivée de Test::Unit::TestCase qui regroupera les tests propres à une classe (ou pas : vous êtes libre, dans Test::Unit, d’en faire ce que vous voulez, mais il est conseillé de faire ainsi, et Rails en fait un par modèle).

require 'test/unit'
require 'Dog'
require 'Cat'
require 'Person'

class DogTest < Test::Unit::TestCase
  def test_should_create_tame_dog_and_feed_him
    doggy    = Dog.new
    grandma  = Person.new "rotten meat"
    assert grandma.dog = doggy, "grandma doesn't want a dog"
    assert_kind_of Animal, doggy, "that dog is weird"
    assert doggy.feed
  end

  def test_should_know_what_is_good_for_him
    doggy    = Dog.new
    kitty    = Cat.new
    grandma  = Person.new "rotten meat"
    assert doggy.feed( kitty )
    assert_raise { doggy.feed grandma }
  end
end

Il s’agit, concrètement, d’un ensemble de méthodes qu’on définira et qu’on nommera chacune test_quelquechose. Le préfixe test_ est obligatoire. On donne généralement un nom représentatif à ce test, étant donné que ce nom apparaîtra si le test échoue. On peut écrire des méthodes sans le préfixe test_ mais elles ne seront donc pas considéré comme des tests et ne seront pas exécutées automatiquement.

Car c’est l’intérêt de cette classe : lorsqu’on exécute un fichier qui require ‘test/unit’, il lance automatiquement les tests les uns après les autres et affiche un rapport à la fin.

Isoler ainsi les assertions dans des testcases a pour intérêt évident une plus grande sémantique et une meilleure organisation des tests, mais cela permet aussi de partager des fixtures. Contrairement à ce que nous avons vu dans l’article précédent, les fixtures de Test::Unit standard ne sont pas des fichiers YAML, c’est un ajout de Rails. Dans les tests standards, on gère les fixtures grâce à deux méthodes rajoutées dans le testcase : setup et teardown.

setup sera appelé avant l’exécution de chaque méthode du testcase. Cela signifie que les fixtures qui sont placés dedans sont réinitialisés entre deux tests. Si vous utilisez des destructeurs, teardown sera exécuté après chaque test (cela peut être intéressant dans une perspective de logging aussi). L’exemple précédent peut donc être simplifié ainsi :

require 'test/unit'
require 'Dog'
require 'Cat'
require 'Person'

class DogTest < Test::Unit::TestCase
  def setup
    @doggy    = Dog.new
    @kitty    = Cat.new
    @grandma  = Person.new "rotten meat"
  end

  def test_should_create_tame_dog_and_feed_him
    assert @grandma.dog = @doggy, "grandma doesn't want a dog"
    assert_kind_of Animal, @doggy, "that dog is weird"
    assert @doggy.feed
  end

  def test_should_know_what_is_good_for_him
    assert @doggy.feed( @kitty )
    assert_raise { @doggy.feed @grandma }
  end
end

Bien qu’utilisant son propre système de fixture, Rails sait aussi se servir de ce système-là, ce qui vous permet d’exécuter du code arbitraire avant et après chaque méthode de test.

Enfin, une testsuite regroupe plusieurs testcases. C’est principalement un outil de convenance pour vous empêcher de devoir exécuter tous vos tests manuellement les uns après les autres, ou de devoir les mettre dans le même fichier. Ainsi, il vous suffit de faire un fichier contenant :

require 'test/unit'
require 'my_tests/DogTest'
require 'my_tests/CatTest'
require 'my_tests/PersonTest'
# etc

Et tous les tests seront lancés à l’exécution de ce fichier.

Voilà, vous avez maintenant une connaissance solide des bases de Test::Unit, nous allons pouvoir revenir à Rails.

Test::Unit dans Rails

Peut-être faut-il lever tout de suite un quipropos. Dans le répertoire test/ d’une application Rails, vous pourrez voir unit/, functional/ et integration/. Le fait qu’un répertoire se nomme unit ne doit pas vous induire en erreur : tous trois utilisent Test::Unit (avec des ajouts propres à Rails), et plus particulièrement, Test::Unit::TestCase, ou dérivé.

On peut noter un changement de focalisation entre les trois, allant de unit/, très précis et particulier, à integration, très abstrait et dont les tests simulent des cas concrets de parcours dans l’application par un utilisateur.

Pour lancer les tests, on peut utiliser une de ces formes de commande :

ruby test/unit/user_test.rb
rake test:units
rake test:functionals
rake test:integration
rake test

La première lance un unique testcase, ici celui du modèle User. Les trois suivants lance l’ensemble des testcases d’une des trois famille de tests. Le dernier lance tous les tests.

Comme je l’ai dit plus haut, Rails permet un gestion des fixtures plus sophistiquée que Test::Unit. Vous avez déjà écrit vos fixtures. Pour les charger dans vos tests, il suffit d’indiquer le nom du fixture que vous voulez charger dans la classe de votre testcase, par exemple :

class AccountTest < ActiveSupport::TestCase
  fixtures :accounts, :users
  # ...
end

Si vous désirez charger tous les fixtures, vous pouvez spécifier :

fixtures :all

Par Ailleurs, vous pouvez utiliser des helpers dans vos tests, que vous placerez dans test/test_helper.rb.

Enfin, Rails rajoute deux méthodes très utiles à Test::Unit::Assertions : assert_difference et assert_no_difference.

Voilà, le plus lourd est passé. Vous avez les bases pour vous servir de l’ensemble des tests de rails. Oui, c’est la petite surprise de la notion de test unitaire dans Rails : elle crée la confusion avec Test::Unit qui est en fait la base de tout.

Étant donné que le terme Test::Unit est peu mentionné dans Rails (il y a même une classe ActiveSupport::Testcase qui ne fait qu’hériter de Test::Unit::TestCase), je parlerai des tests relatifs à test/unit/ lorsque je parlerai de tests unitaires.

Au volant!

Voyons maintenant comment se servir des tests unitaires dans le contexte du test driven development.

L’implémentation MVC de Rails est fortement portée sur les modèles. Cela veut dire que vous devez avoir des modèles très chargés et des contrôleurs très maigres (je ne parviens pas à retrouver la citation exacte, désolé).

Pourtant l’écriture des tests des modèles, préalable à l’écriture des modèles même, n’est pas si conséquente que cela. La raison en est surtout que vous saurez de quoi à besoin votre modèle lorsque vous penserez vos contrôleurs.

Une première écriture de test de modèle se concentre donc généralement sur le test du CRUD et des validations. Un exemple typique sera donc :

require 'test_helper'

class AccountTest < ActiveSupport::TestCase
  fixtures :accounts, :users
  # create
  def test_should_create
    assert_difference 'Account.count' do
      Account.create :user => users( :quentin), :name => 'test_should_create_account_name'
    end
  end

  def test_validations
    assert_no_difference 'Account.count' do
      account = Account.create
      assert_not_nil account.errors.on( :user_id ), "Un compte appartient à un user"
      assert_not_nil account.errors.on( :name ), "Les comptes sont distingués par leur nom par les visiteurs"
    end
  end

  def test_name_should_be_unique
    assert_no_difference 'Account.count', "Les comptes sont distingués par leur nom par les visiteurs" do
      account = Account.create :user => users( :quentin), :name => accounts( :account1 ).name
      assert_not_nil account.errors.on( :name )
    end
  end

  # read
  def test_should_read
    account = Account.create :user => users( :quentin ), :name => "test_read_account_name"
    assert_nothing_raised do
      second = Account.find_by_name 'test_read_account_name'
      assert_equal second.id, account.id
    end
  end

  # update
  def test_should_update
   assert (accounts( :account1 ).update_attributes :name => "test_update_account_name")
  end

  # delete
  def test_should_delete
    assert_difference 'Account.count', -1 do
      users( :quentin ).destroy
    end
  end

  def test_all_clear
    flunk
  end
end

On test donc principalement ici les tâches classique du modèle et les validations. Ce n’est pas insignifiant. Lorsque vous vous enfoncerez dans le développement de votre application et que vous écrirez d’autres modèles, il y a de fortes chances pour que vous n’ayez plus bien en tête ce qui se passe dans ce modèle-ci. Lancer régulièrement les tests vous assurera de la solidité de votre modèle.

De plus, vous vous êtes sûrement déjà retrouvé dans le cas où des bugs sont apparus parce que vous avez modifié du code dont vous ne vous souveniez plus du sens. Pour éviter ça, je vous conseille d’abuser des messages personnalisés des assertions. Plutôt que de simplement répéter la raison pour laquelle le test échoue, et qui est déjà comprise dans le nom de la méthode de test, votre message personnalisé peut expliquer pour quelles raisons contextuelles le test doit passer. C’est le meilleur moyen pour vous rappeler ce que vous avez fait sur les fichiers précédents sans devoir aller les rouvrir, chercher d’où provient l’erreur et lire les éventuels commentaires que vous y avez laissé.

Revenons à la décortication de l’exemple.

Vous avez peut-être remarqué que l’expression passée à assert_difference ou assert_no_difference est un string. C’est indispensable. C’est fonction évalue bien une différence numérique, mais elle doivent avoir en paramètre un expression dans un string qui sera passé à eval. La raison de cela est que cette expression est évaluée deux fois : une fois au début, une fois à la fin. La soustraction est faite ensuite entre le résultat final et le résultat originel. assert_difference prend en second argument la valeur que doit avoir cette soustraction. Cela implique qu’on doit la spécifier si on veut ajouter un message personnel (troisième argument). La valeur par défaut est 1, ce qui veut dire que assert_difference test par défaut s’il y a eu un ajout sur l’expression, typiquement : le nouvel objet a-t’il été enregistré?

users( :quentin ) et accounts( :account1 ) permettent de retrouver des fixtures selon leur nom.

Un dernier détail à propos de ce code, la dernière méthode. flunk est une fonction permettant de faire échouer systématiquement le test. C’est en fait un raccourci pour assert false. Lors de la première écriture d’un test et de son modèle (ou autre), une dernière méthode avec un flunk permet de signifier qu’il y a encore des choses à écrire. Votre fichier de test, outre le fait d’être un observer et un reminder, est aussi une todo list. Lorsque vous aurez fini la première écriture de votre modèle et que son test passera sans problème, vous pourrez supprimer la méthode de flunk.

Conclusion

Vous venez d’avaler la partie la plus complexe pour qui découvre les outils de tests de Rails. Les tests d’intégration et fonctionnels seront plus complexes, mais il réutiliseront les même outils et structures, avec quelques méthodes ajoutées par Rails pour vous simplifier la vie.

Dans le cadre du Test Driven Development, vous aviez d’abord écrit les relations de vos modèles et vos migrations grâce aux fixtures, vous écrivez maintenant les validations et peut-être quelques méthodes de vos modèles grâce aux tests unitaires.

Vous reviendrez sur ces tests, lorsque vous vous attaquerez à vos contrôleurs et que vous voudrez ajouter de nouvelles méthodes à vos modèles. Vous créerez probablement d’autres modèles, également, en fonction des précisions que votre client vous fournira, des nouvelles requêtes et de tous les imprévus qui pimentent la vie d’un développeur. Notez d’ailleurs que dans le cadre d’un développement agile et des fameux phantasmes de “je suis en face du client, il me demande une modif, je change deux lignes, je lui montre, il n’en revient pas”, les tests vous seront d’un grand secour, afin que ça ne devienne pas “je suis en face du client, il me demande une modif, je change deux lignes, ça casse tout, je cherche le bug, il s’impatiente, je lui dis que ça prendra deux minutes, je comble le vide en baratinant du discours technique, je trouve pas, ça m’enerve”.

Ce qui compte, c’est que lorsque vous en aurez fini avec l’écriture de ces tests et des modèles qu’ils couvrent, vos modèles seront pleinement opérationnels et solides. Il est alors temps de s’attaquer aux contrôleurs (oui, vous avez fait tout ça en une journée, hormis la map initiale).

Share and Enjoy:
  • del.icio.us
  • Digg-Design
  • Blogasty
  • Fuzz
  • Scoopeo

Laisser un commentaire