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
- 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-envjs, capybara-zombie, akephalos, capybara-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