Lorsqu’on commence à réaliser une application Rails, on est vite tenté de ne pas faire de tests. Mettre en place une stack de test peut prendre du temps. La diversité des tests de Rails peut effrayer. On peut juger que pour la taille du projet, passer par l’étape des tests est inutile. Enfin, les raisons ne manquent pas pour faire l’impasse sur cette partie du développement.
J’avais déjà présenté Capybara dans un précédent article. Aujourd’hui, je voudrais non pas débattre sur les bénéfices du tests mais simplement partager une stack de tests avec vous.
La première brique c’est bien entendu Rails. Rails définit un tas de conventions concernant les tests.
Par défaut Rails utilise un système de fixtures, la librairie test/unit
, fournit des générateurs, etc.
Tout ceci est très bien expliqué dans le guide du test avec Rails (en).
Pour utiliser cela, il n’y a donc pas de dépendances à ajouter a votre Gemfile
.
Il vous faudra par contre configurer votre environnement de test et votre base de données de test.
En ce qui me concerne, j’utilise Postgresql et Sequel par défaut.
J’ai donc les lignes suivantes dans mon Gemfile
:
# Gemfile
# Replace sqlite3 by postgresql
#gem 'sqlite3'
gem 'pg'
gem 'sequel'
gem 'talentbox-sequel-rails', require: 'sequel-rails'
La dernière ligne : sequel-rails permet d’intégrer Sequel dans Rails.
La gem adapte les générateurs pour les migrations et les modèles, les taches rakes, la connexion à la base, etc.
Pensez à faire un bundle install
.
Afin de faire entrer Sequel dans Rails, il faut se débarrasser de tout ce qui concerne ActiveRecord dans les fichiers de configuration. Voyez le site de sequel-rails pour les détails d’installation.
Voici mon fichier config/database.yml
:
# config/database.yml
development: &dev
adapter: postgresql
username: myapp_user
database: myapp_development
test:
<<: *dev
database: myapp_test
Sécurité de la base de donnée
L’utilisateur myapp_user
ne dispose pas des droits nécessaires à la création/suppression de bases de données.
En conséquence, il me faut créer les deux bases à la main puis donner les droits a myapp_user
sur celles ci.
Les taches db:create
ou db:test:prepare
sont également inadaptées.
L’environnement de test s’en trouve perturbé mais il me semble que la raison justifie le dérangement.
Dans le fichier lib/tasks/db.rake
, je garde une tache qui me permet de supprimer une a une les tables de la base puis d’y charger le fichier db/schema.rb
:
# lib/tasks/db.rake
namespace :db do
namespace :test do
desc "Clean all the table in the database and load the schema.rb file"
task recreate: :environment do
if Rails.env.test?
Sequel::DATABASES.each do |db|
puts 'Drop tables:'
db.tables.each do |table|
db.drop_table?(table, cascade: true)
puts " #{table}"
end
end
Rake::Task['db:schema:load'].invoke
else
abort "This is only for test env."
end
end
end
end
Après chaque nouvelle migration, je lance donc la commande RAILS_ENV=test bundle exec rake db:test:recreate
.
Plutôt que d’utiliser le framework par défaut, j’utilise RSpec. Pour moi l’avantage majeur est qu’il très bien documenté, ne ratez surtout pas la documentation de RSpec.
Rspec se divise en 4 parties :
Pour ajouter RSpec à son projet, commencez par ajouter un groupe test
à votre Gemfile
:
# Gemfile
group :development, :test do
# Use RSpec for testing
gem 'rspec-rails'
end
Cette section du gemfile va grossir au fur et à mesure que l’on ajoutera des éléments à notre stack de tests.
Ensuite exécutez les commandes suivante pour compléter l’installation et lancer la suite de tests :
bundle install bundle exec rails generate rspec:install bundle exec rake spec
Si vous suivez ce billet en ayant un pied dans votre console, la dernière ligne de commande vous lancera un No examples matching ./spec{,/*/**}/*_spec.rb could be found
.
Si vous n’avez rien en base c’est normal.
Vous avez pu remarquez que ma solution de supprimer les tables lors du db:test:recreate
est brutale. Vous devez recharger d’éventuelles données après chaque migrations.
En ce qui me concerne, je pars toujours d’une base vierge. J’ai beaucoup trop de mal à maintenir un ensemble de fixtures cohérent.
Pour m’aider dans la génération du contexte de mes tests, j’utilise une factory pour mes objets. Je confie ce travail à Fabrication, qui est simple et compatible avec beaucoup d’ORMs.
Pour l’installer, on modifie la section test
de notre Gemfile
qui devient :
group :development, :test do
# Use RSpec for testing
gem 'rspec-rails'
# Factory for sequel models
gem 'fabrication'
gem 'faker'
end
J’ai également ajouté la gem Faker qui me semble maintenant indispensable pour générer de fausses données de test.
Pour compléter l’installation de Fabrication, on insère dans le fichier config/application.rb
les lignes suivantes :
config.generators do |g|
g.test_framework :rspec, fixture: true
g.fixture_replacement :fabrication
end
Les tests réalisés doivent être entièrement indépendants. RSpec sait mettre à profit les transactions SQL uniquement lorsque l’on utilise ActiveRecord.
DatabaseCleaner fournit plusieurs moyens de nettoyage de la base de donnée pour les ORMs les plus populaires.
On modifie la section test
de notre Gemfile
qui devient :
group :development, :test do
# Use RSpec for testing
gem 'rspec-rails'
# Factory for sequel models
gem 'fabrication'
gem 'faker'
# Use database cleanner to clean data after each tests
gem 'database_cleaner'
end
Une fois installé, on configure le nettoyage dans le fichier spec/spec_helper.rb
généré lors de l’installation de RSpec :
require 'database_cleaner'
RSpec.configure do |config|
# Do not use ActiveRecord's transactionnal fixtures
#config.use_transactional_fixtures = true
# Wrap each test in a transaction
DatabaseCleaner.strategy = :transaction
config.around(:each) do |spec|
DatabaseCleaner.start
spec.run
DatabaseCleaner.clean
end
end
Pour relancer automatiquement la suite de tests lorsqu’un fichier est modifié, on va utiliser Guard et particulièrement son plugin Guard RSpec. Et, afin de recharger le minimum d’élements on va utiliser Spork et le plugin associé Guard Spork.
On commence par ajouter guard
à notre section de test :
group :development, :test do
# Guard
gem 'rb-inotify', '~> 0.8.8', require: (RUBY_PLATFORM.downcase.include?('linux') && 'rb-inotify')
gem 'rb-fsevent', require: (RUBY_PLATFORM.downcase.include?('darwin') && 'rb-fsevent')
gem 'guard'
# Use RSpec for testing
gem 'rspec-rails'
# Factory for sequel models
gem 'fabrication'
gem 'faker'
# Use database cleanner to clean data after each tests
gem 'database_cleaner'
end
Puis on exécute :
bundle install bundle exec guard init
Ensuite on ajoute guard-rspec
, spork
et guard-spork
:
group :development, :test do
# Guard
gem 'rb-inotify', '~> 0.8.8', require: (RUBY_PLATFORM.downcase.include?('linux') && 'rb-inotify')
gem 'rb-fsevent', require: (RUBY_PLATFORM.downcase.include?('darwin') && 'rb-fsevent')
gem 'guard'
# Use RSpec for testing
gem 'rspec-rails'
gem 'guard-rspec'
# Spork test server to avoid restarting the full Rails stack
gem 'spork'
gem 'guard-spork'
# Factory for sequel models
gem 'fabrication'
gem 'faker'
# Use database cleanner to clean data after each tests
gem 'database_cleaner'
end
Pour finaliser l’installation, outre le classique bundle install
, on doit initialiser Spork :
bundle install bundle exec spork rspec --bootstrap
Cette dernière commande a pour effet de préparer le fichier spec/spec_helper.rb
a être séparé en deux.
En effet, on sépare en deux ce fichier en fonction de ce qu’il faut charger au démarrage (prefork) et ce qu’il ne faut recharger qu’à chaque test (each_run).
require 'rubygems'
require 'spork'
Spork.prefork do
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'
require 'database_cleaner'
end
Spork.each_run do
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
RSpec.configure do |config|
config.fixture_path = "#{::Rails.root}/spec/fixtures"
config.infer_base_class_for_anonymous_controllers = false
config.order = "random"
# Wrap each test in a transaction
DatabaseCleaner.strategy = :transaction
config.around(:each) do |spec|
DatabaseCleaner.start
spec.run
DatabaseCleaner.clean
end
end
end
En plus de ce fichier, on doit modifier le fichier Guardfile
afin de prendre en compte RSpec et Spork.
Voici un exemple :
# Guardfile
guard 'spork', rspec_env: { 'RAILS_ENV' => 'test' }, test_unit: false, cucumber: false do
watch('config/application.rb')
watch('config/environment.rb')
watch('config/environments/test.rb')
watch(%r{^config/initializers/.+\.rb$})
watch('Gemfile')
watch('Gemfile.lock')
watch(%r{^lib/(.+)\.rb$})
watch('spec/spec_helper.rb') { :rspec }
watch('test/test_helper.rb') { :test_unit }
watch(%r{features/support/}) { :cucumber }
end
guard 'rspec' do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
watch('spec/spec_helper.rb') { "spec" }
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb"] }
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
watch('app/controllers/application_controller.rb') { "spec/controllers" }
end
Une fois tous ces fichiers en place on peut lancer la commande bundle exec guard
afin de surveiller les fichiers listés dans le Guardfile
et de lancer les actions appropriées.
Parmi la grande variété de tests que recommande Rails, il y a les tests d’intégrations.
Avec RSpec ils sont générés dans le répartoire spec/features
.
Ces tests nécessitent l’utilisation de Capybara dans sa version 2.0.0 ou plus.
On l’ajoute donc à notre section test
qui devient :
group :development, :test do
# Guard with the GNU/Linux inotify library to get file changes instead of polling
gem 'rb-inotify', '~> 0.8.8', require: (RUBY_PLATFORM.downcase.include?('linux') && 'rb-inotify')
gem 'rb-fsevent', require: (RUBY_PLATFORM.downcase.include?('darwin') && 'rb-fsevent')
gem 'guard'
# Use RSpec for testing
gem 'rspec-rails'
gem 'guard-rspec'
# Use Capybara's DSL to test features
gem 'capybara'
# Spork test server to avoid restarting the full Rails stack
gem 'spork'
gem 'guard-spork'
# Factory for sequel models
gem 'fabrication'
gem 'faker'
# Use database cleanner instead of transaction for advanced JS tests
gem 'database_cleaner'
end
Il faut également ajouter à notre fichier spec/spec_helper.rb
, dans la section prefork, la ligne suivante :
require 'capybara/rails'
Par défaut, Capybara va utiliser un driver basé sur Rack-test pour exécuter les tests d’intégration. Ce driver est rapide puisqu’il ne charge pas de navigateur mais communique directement avec Rack.
Dans beaucoup de situations le javascript fait partie de la fonctionnalité à tester. Dans ces cas là, Rack-test ne suffit pas. Pour palier à ce problème, j’utilise Poltergeist : un driver pour Capybara qui utilise PhantomJS pour jouer les tests. Dans mon exemple j’utilise la version de développement de Poltergeist qui est compatible avec Capybara 2, ce qui n’est pas encore le cas pour la version stable.
group :development, :test do
# Guard with the GNU/Linux inotify library to get file changes instead of polling
gem 'rb-inotify', '~> 0.8.8', require: (RUBY_PLATFORM.downcase.include?('linux') && 'rb-inotify')
gem 'rb-fsevent', require: (RUBY_PLATFORM.downcase.include?('darwin') && 'rb-fsevent')
gem 'guard'
# Use RSpec for testing
gem 'rspec-rails'
gem 'guard-rspec'
# Use Capybara's DSL to test features and poltergeist drivers for advanced tests
gem 'capybara'
gem 'poltergeist', github: 'jonleighton/poltergeist', branch: 'master'
# Spork test server to avoid restarting the full Rails stack
gem 'spork'
gem 'guard-spork'
# Factory for sequel models
gem 'fabrication'
gem 'faker'
# Use database cleanner instead of transaction for advanced JS tests
gem 'database_cleaner'
end
Bien sûr, vous devez avoir installé PhantomJS pour utiliser ce driver.
Lorsqu’on utilise un driver différent de Rack-test, Capybara utilise un thread séparé pour jouer le rôle de serveur.
Dans ce cas, jouer chaque test dans une transaction ne fonctionne plus.
En effet, la mise en place du test se fait dans une transaction et les requêtes se font hors de cette transaction.
Pour utiliser le driver uniquement lorsque c’est nécessaire et pour utiliser une autre stratégie de nettoyage de la base de données, modifier le fichier spec/spec_helper.rb
comme ceci :
# Wrap each test in a transaction
config.around(:each) do |spec|
if example.options[:phantom]
Capybara.current_driver = :poltergeist
DatabaseCleaner.strategy = :truncation
else
Capybara.current_driver = :rack_test
DatabaseCleaner.strategy = :transaction
end
DatabaseCleaner.start
spec.run
DatabaseCleaner.clean
end
Ensuite, pour utiliser ponctuellement phantomjs, on pourra ajouter l’option :phantom
au test d’intégration.
scenario 'some extra feature', phantom: true do
...
end
Lorsque l’on teste son code, on veut être sûr de ne rien oublier. Simplecov fournit des rapport de couverture assez simples.
On l’ajoute à notre section test
qui devient :
group :development, :test do
# Guard with the GNU/Linux inotify library to get file changes instead of polling
gem 'rb-inotify', '~> 0.8.8', require: (RUBY_PLATFORM.downcase.include?('linux') && 'rb-inotify')
gem 'rb-fsevent', require: (RUBY_PLATFORM.downcase.include?('darwin') && 'rb-fsevent')
gem 'guard'
# Use RSpec for testing
gem 'rspec-rails'
gem 'guard-rspec'
# Use Capybara's DSL to test features and poltergeist drivers for advanced tests
gem 'capybara'
gem 'poltergeist', github: 'jonleighton/poltergeist', branch: 'master'
# Spork test server to avoid restarting the full Rails stack
gem 'spork'
gem 'guard-spork'
# Factory for sequel models
gem 'fabrication'
gem 'faker'
# Use database cleanner instead of transaction for advanced JS tests
gem 'database_cleaner'
# Use simplecov to get test covering metrics
gem 'simplecov', :require => false
end
Puis on ajoute en tête de notre bloc prefork les lignes suivantes :
Spork.prefork do
unless ENV['DRB']
require 'simplecov'
SimpleCov.start 'rails'
end
...
end
Après l’exécution de vos tests par bundle exec rake spec
, vous trouverez un rapport complet dans ce fichier HTML : coverage/index.html
.
Voici, en vrac, divers fonctionnalitées ou hacks bons à connaitre.
Pour un upload de fichier dans un test d’intégration, capybara permet de faire ceci :
attach_file 'resource[file]', "#{Rails.root}/spec/fixtures/files/1.js"
Lors du test d’un controlleur, on passera par cette écriture :
post :action, f: Rack::Test::UploadedFile.new("#{Rails.root}/spec/fixtures/files/1.js")
Selon votre répertoire d’upload, pensez a nettoyer les fichiers uploadés après chaque suite de tests exécutée.
Pour cela, ajoutez cette ligne dans le fichier spec/spec_helper.rb
, dans la configuration de RSpec :
# Remove all the uploaded files
config.after(:all) { FileUtils.rm_rf(Rails.root.join('uploads', 'test')) }
Si vous avez des contraintes d’hôte dans vos routes, vous devez les simuler lors de vos test d’intégration.
Avec Rack-test, pas de problème, un simple Capybara.app_host = 'http://test.host'
et le tour est joué.
Avec Poltergeist, il va falloir ajouter la ligne suivante dans votre fichier /etc/hosts :
127.0.0.1 test.host
et, spécifier le port sur lequel tourne le serveur de capybara :
Capybara.app_host = "http://test.host:#{Capybara.current_session.server.port}"
Lors du test des controlleurs, vous avez directement la possibilité de modifier la requête : request.host = 'test.host'
.
Pour ajouter ou supprimer des cookies dans mes tests d’intégration, j’utilise deux méthodes définies dans un helper RSpec :
def get_cookie(name)
case Capybara.current_driver
when :rack_test
sess = Capybara.current_session.driver.browser.current_session
sess.instance_variable_get(:@rack_mock_session).cookie_jar[name]
when :poltergeist
Capybara.current_session.driver.cookies[name].try(:value)
else
raise "Unknown driver used"
end
end
def set_cookie(name, value)
case Capybara.current_driver
when :rack_test
sess = Capybara.current_session.driver.browser.current_session
sess.instance_variable_get(:@rack_mock_session).cookie_jar[name]=value
when :poltergeist
if Capybara.app_host
h = URI.parse(Capybara.app_host).host
b = Capybara.current_session.driver.browser
b.set_cookie(name: name, value: value, domain: h)
else
Capybara.current_session.driver.set_cookie(name, value)
end
else
raise "Unknown driver used"
end
end
Ce sont deux hacks, tant pour l’accès au cookie_jar
du driver Rack-test que pour la définition de cookies avec Poltergeist lorsque Capybara.app_host
commence par http://
.
Lors du test des controlleurs, vous avez directement accès aux cookies avec : response.cookies['name']
ou request.cookies['name']=value
.
Si vous faites du controle d’accès via l’IP directement dans votre application Rails, vous serez amené à changer l’IP du faux client lors de tests d’intégration.
Toujours dans un module d’helpers j’utilise le hack suivant :
def customize_ip(cip)
@_custom_ip = ENV['FAKE_TEST_IP'] = cip
end
def custom_ip
@_custom_ip || '127.0.0.1'
end
module ::ActionDispatch
class Request < Rack::Request
def remote_ip
@remote_ip ||= (ENV.delete('FAKE_TEST_IP') || @env["action_dispatch.remote_ip"] || ip).to_s
end
end
end
Cette technique ne fonctionne que pour la prochaine requête reçu par le serveur mais ouvre la voie vers d’autres solutions.
Voila en ce qui concerne ma stack actuelle, si vous avez des suggestions sur ce qu’il y a en trop, sur ce qu’il manque ou sur d’autres hacks utiles alors n’hésitez pas à me le faire savoir. Avec ces infos vous devriez pouvoir mettre en place une stack de tests en un rien de temps, avant même d’avoir le moindre modèle ou la moindre migration.
L’équipe Synbioz.
Libres d’être ensemble.