Rails += Tests

Si vous avez déjà créé une application Ruby on Rails, vous avez déjà dû voir un étrange répertoire : tests.

N'ayez pas peur, tout a été fait pour faciliter la mise en place de tests de bout en bout avec Rails.

Je vais donc vous donner les méthodes que j'apprécie et que je considère efficaces pour l'écriture de tests en Rails. Que vous soyez novices ou expert, j'espère pouvoir vous en apprendre un peu.

Tous les exemples donnés seront pour Rails 3, mais ils sont pratiquement tous compatible Rails 2.

Le code source des exemples est disponible sur ce github.

Tests unitaires

Les tests les plus basiques, les tests unitaires, permettent de tester vos modèles unitairement. Mais pour ce faire vous avez souvent besoin de données standard. La méthode intégrée à Rails, les fixtures, est à mon goût peu efficace. Il est compliqué et rébarbatif de faire des allers-retours entre son test et le fichier de définition des fixtures. C'est pour ça que je préfère utiliser FactoryGirl. Cette gem permet de créer des "Factory" qui sont une fabrique pour vos modèles.

On peut spécifier des valeurs par défaut, mais on peut tout aussi bien surcharger celles-ci.

Dans l'exemple suivant on va définir :

  • Des modèles
    • Un utilisateur User
    • Des billets de blog Post qui est écrit par un utilisateur
  • Des factories
    • Une factory par modèle, qui définie des valeurs par défaut
  • Des séquences
    • Une séquence est un générateur de données

Les factories

Factory.define :user do |u| 
  u.name { Factory.next(:name) }  #Utilisation d'une sequence
  u.email { Factory.next(:email) }
end

Factory.define :post do |p|
  p.title "A title" #Définition d'une valeur par défaut
  p.text "My long blog post" 
  p.association :user #Association automatique d'un post avec un user.
end

Les séquences

Factory.sequence :email do |n| 
  "person#{n}@octo.com"
end

Exemple, tiré de PostTest

test "factories" do
    post = Factory :post #Création d'un post 'par défaut'
    post2 = Factory :post, :title => "My new title" #Création d'un post avec comme titre 'My new title'

    assert_equal "A title", post.title
    assert_equal "My new title", post2.title
    assert_not_nil post.user
  end

Je ne parlerai pas de RSpec car je ne vois pas la valeur ajoutée par rapport aux tests unitaires de Ruby.

De même que RSpec, je trouve la gem shoulda assez peu utile, d'autant plus si on utilise pas RSpec.

Mais, la gem shoulda-matchers est très intéressante. Elle permet, par exemple, de tester les validations et associations ActiveRecord en une seule ligne. Je vous laisse consulter la documentation pour voir tous les matchers.

Par exemple (UserTest) :

should have_many(:posts) #On vérifie qu'un utilisateur à bien plusieurs Post
  should have_many(:comments) 

  describe User do
    before(:each) { Factory :user }
    should validate_uniqueness_of(:name) #On vérifie qu'un utilisateur doit avoir un nom unique
  end

Tests fonctionnels

Ce sont les tests que je considère les moins intéressants en rails. Ils peuvent très facilement être écrit en tests d'intégrations (que je traite juste d'après), ce qui les rend plus facile à maintenir. À mon avis, ils ne servent que dans le cas où les actions rails sont attaquées directement. Par exemple : des webservices, des actions d'auto-complétions, des appels AJAX.

Je ne connais aucune gem pour simplifier/améliorer ces tests. J'utilise donc que les Factory et l'API que rails propose. Je ne vais pas présenter exemples, mais vous pouvez toujours regarder cet exemple.

Tests d'intégrations

Un test d'intégration a pour but de tester de bout en bout une fonctionnalité d'une application. Ils sont à mon avis les plus importants. Mais ce sont aussi ceux qui nécessitent le plus d'attention. En effet, il est impossible et pas nécessaire de tester tout (affichage de la page, du texte, du css, etc.). Il est important de ne se focaliser que sur les tests les plus importants. De plus, qui dit application web, dit JavaScript. Et tester du JavaScript sans navigateur n'est pas toujours facile.

Note pour Rails 3 :

Les tests d'intégrations ne sont plus générés automatiquement avec le scaffolding. Il faut donc lancer à la main la commande :

rails generate integration_test MyTest

Webrat et Capybara

Webrat et Capybara sont deux gems qui permettent de faire des tests d'interface web en Ruby. On peut bien évidemment les utiliser avec Rails. Ces deux gems fonctionnent toutes les deux sur un DSL qui représente les actions qu'un humain peut effectuer sur un site internet : cliquer sur un lien, remplir un champs, etc.

Webrat a été le premier à exister. Capybara est une réécriture qui se veut plus souple et plus modulaire. Il permet l'utilisation de drivers qui fournissent des fonctionnalités différentes. Pour la petite histoire, le capibara est le plus gros rat du monde. Webrat est néanmoins moins actif que Capybara.

Note pour Rails 2 :

Avec Rails 2, les tests rails utilisant Webrat peuvent utiliser l'API des tests d'intégrations de rails. Ce qu'il est impossible de faire avec Capybara. Par exemple, on ne peut pas utiliser assert_template avec ce dernier.

Passons aux exemples :

setup do
    @user = Factory :user #Créé un utilisateur
  end

  test "capybara" do
    visit posts_path #Va sur la page /posts
    click_link 'New Post' #Click sur le lien nommé 'New Post'
    fill_in "Text", :with => "My blog post" # Rempli le champs nommé 'Text' avec 'My blog post'
    fill_in "User", :with => @user.name

    assert_difference "Post.count" do
      click_button "Create Post" # Click sur le bouton nommé "Create Post"
    end

    assert_equal post_path(Post.last), current_path
    assert_true page.has_content?("Post was successfully created.")
  end

Comme je l'expliquais au dessus, le DSL de Capybara est assez clair. La méthode visit permet d'aller à une page, fill_in de remplir un champs, click_(link|button) de cliquer sur un lien ou bouton. Dans notre exemple on fait un fill_in "Text", :with => "My blog post", et Capybara comprend que l'on veut remplir le champs texte associé au label "Text" avec la valeur "My blog post". Il (ou Webrat) est assez "intelligent" pour savoir à quoi correspond les chaînes de caractères des paramètres. En effet, on peut soit spécifier du texte pur, des ids CSS ou même du XPath.

N'oublions pas la méthode save_and_open_page qui permet de rendre la page tel que Capybara est en train de la "voir", très pratique pour debugger.

Les drivers Capybara

La légende dit que l'on peut tester du JavaScript avec Capybara. J'ai testé 4 drivers (capybara-envjscapybara-zombieakephaloscapybara-webkit) et je n'ai pas réussi à en faire fonctionner un seul. Si vous arrivez à tester l'auto-complétion de l'application de tests partagez votre savoir car il vaut de l'or. Le code du test est ici.

Autres types de tests

Tests de performance

Les tests de performance permettent de benchmarker les performances de votre application. On les génère avec la commande rails generate test_unit:performance [TestName]. Il ne faut pas oublier de rajouter la gem ruby-prof comme dépendance.

Attention, ces tests ne permettent pas de vérifier si votre application sera rapide ou pas. Ils servent plutôt à voir comment votre application utilise la VM Ruby en terme de mémoire, de garbage collector, etc.

Ils se lancent de deux façons :

  • rake test:benchmark
  • rake test:profile

Le mode benchmark lance chaque test 4 fois, mais ne donne pas de résultat sur l'utilisation du garbage collector. Le mode profile fait l'inverse.

Pour plus de détails référez vous à la documentation de rails sur le sujet.

Tests de mailer

Il n'y a que 3 choses à connaître pour tester les mailers.

  • On peut les tester dans tous les types de tests (unitaire, fonctionnel, intégration)
  • Si on veut vérifier qu'un mail est bien envoyé on regarde le tableau ActionMailer::Base.deliveries
  • Pour récupérer les mails envoyés on regarde le même tableau

Par exemple donc (tiré de la documentation de rails) :

assert_difference 'ActionMailer::Base.deliveries.size', +1 do
  post :invite_friend, :email => 'friend@example.com'
end

invite_email = ActionMailer::Base.deliveries.first

Cucumber

Je ne vais pas rentrer dans le détail de Cucumber puisqu'il y a déjà un article à son sujet.

Autour des tests

Les tâches rake

Rails fournit de base plusieurs tâches rake pour les tests. Vous pouvez en avoir la liste par la commande rake -T test

  • rake test : Lance tous les
  • rake test:units, test:functionals, test:integration, test:benchmark, test:profile, test:plugins : Lancent les tests du même nom
  • rake test:recent : Ne lance que les tests qui ont été sauvegardé récemment
  • rake test:uncommitted : Ne lance que les tests non commités (ne fonctionne qu'avec svn et git)

Timecop

Timecop est une gem qui permet de stopper ou de voyager dans le temps. Malheureusement, ça ne fonctionne que dans des tests ruby/rails. Allons voir quelques exemples :

test "Timecop" do
    time_freeze = 5.days.ago
    Timecop.freeze(time_freeze) do #Stop le temps pour la durée du bloc
      comment = Factory :comment
      comment.update_attribute :text, "New test"
      assert_equal time_freeze, comment.updated_at
    end

    time_travel = 6.days.from_now
    Timecop.travel(time_travel) #Voyage dans le temps
    assert_equal time_travel.to_date, Time.now.to_date
    Timecop.return #Remet à zéro le temps
  end

Mocha

Mocha est à mon avis la façon la plus simple de mocker des méthodes en ruby/rails. Cette gem fonctionne aussi bien sur les méthodes d'instances que sur les méthodes de classe. Si vous avez besoin de mocker les classes TIme ou Date utilisez Timecop.

Voici quelques exemples tirés de la classe CommentTest :

test "mocha" do
    comment = Factory :comment
    Comment.expects(:find).with(1).returns(comment) #On mock la méthode 'find' de 'Comment' pour qu'elle envoie toujours 1
    assert_equal comment, Comment.find(1)

    comment = Factory :comment
    comment.expects(:save).returns(true) #On mock la méthode 'save' de l'objet 'comment' pour qu'elle renvoie toujours 'true'
    assert_true comment.save

    Comment.any_instance.stubs(:text).returns('stub text') #On mock la méthode 'text' de toutes les instances de 'Comment'
    assert_equal 'stub text', Factory(:comment).text
    assert_equal 'stub text', Factory(:comment).text
  end

Conclusion

Éditer votre fichier Gemfile et rajoutez-y ça :

group :test do
  gem 'factory_girl_rails'
  gem 'test-unit', :require => false
  gem 'shoulda'
  gem 'capybara'
  gem 'launchy'
  gem 'mocha', :require => false
  gem 'timecop'
end

Bon tests !