Faciliter ses tests Capybara avec des Page Objects

Chez Tinci, notre stack de tests Rails est basée sur RSpec et Capybara. Avec le temps nous avons appris à réduire la complexité de nos tests avec quelques techniques. L’une d’entre elles est l’utilisation de Page Objects.

Note : Pour des raisons de lisibilité, le code des exemples suivants est volontairement simpliste et parfois dupliqué.

Qu’est-ce qu’un Page Object ?

Un Page Object est une classe qui représente une page spécifique du site. Cela permet de regrouper toutes les informations relatives à sa structure ou à son URL à un seul endroit.

Prenons par exemple la spec suivante :

RSpec.feature 'User sign in form' do
  before(:all) do
    # Mot de passe par défaut 'password'
    FactoryGirl.create(:user, email: '[email protected]')
  end

  scenario 'User signs in successfully' do
    visit '/user/sign_in'
    fill_in 'Email', with: '[email protected]'
    fill_in 'Password', with: 'password'
    click_button 'Sign in'

    expect(current_path).to eq '/'
    expect(page).to have_content 'Successfully signed in !'
  end

  scenario 'User fails to sign in' do
    visit '/user/sign_in'
    fill_in 'Email', with: '[email protected]'
    fill_in 'Password', with: 'wrong'
    click_button 'Sign in'

    expect(current_path).to eq '/user/sign_in'
    expect(page).to have_content 'Incorrect email or password.'
  end
end

Bien que la spec soit assez simple, les répétitions sont nombreuses. Que ce soit les URLs, les étapes pour remplir le formulaire ou le soumettre.

Nous allons voir comment utiliser les Page Objects pour rationaliser les différents aspects de l’interaction avec notre page.

Commençons par créer une classe qui va représenter notre page de connexion :

class SignInPage
end

Nous pouvons maintenant extraire de spec notre tout ce qui est spécifique à cette page : son URL, la saisie du formulaire et le message d’erreur.

class SignInPage
  include Capybara::DSL

  def path
    '/user/sign_in'
  end

  def visit
    super path
    self
  end

  def fill_in_form_correctly(email)
    fill_in_form email: email, password: 'password'
  end

  def fill_in_form_with_errors(email)
    fill_in_form email: email, password: 'wrong'
  end

  def fill_in_form(email:, password:)
    fill_in 'Email', with: email
    fill_in 'Password', with: password
    click_button 'Sign in'
  end

  def has_an_error_message?
    has_content? 'Incorrect email or password.'
  end
end

Voilà une classe bien remplie ! Voyons un peu ce qu’elle contient.

include Capybara::DSL

Inclure le DSL de Capybara permet à notre classe de bénéficier des méthodes de manipulation de page de façon transparente. À noter qu’inclure ce module dans nos Page Objects évite d’avoir à l’inclure de façon globale.

def path
  '/user/sign_in'
end

Cette méthode permet de centraliser la connaissance de l’URL de la page.

def visit
  super path
  self
end

Ici, nous profitons de la méthode visit fournie par Capybara en lui donnant l’URL de notre page. En plus de cela, nous renvoyons la page elle-même pour pouvoir directement chaîner un autre appel de méthode.

def fill_in_form_correctly(email)
  fill_in_form email: email, password: 'password'
end

def fill_in_form_with_errors(email)
  fill_in_form email: email, password: 'wrong'
end

def fill_in_form(email:, password:)
  fill_in 'Email', with: email
  fill_in 'Password', with: password
  click_button 'Sign in'
end

Ces trois méthodes permettent de formuler clairement l’action effectuée tout en centralisant la procédure de saisie du formulaire de connexion.

def has_an_error_message?
  has_content? 'Incorrect email or password.'
end

Pour finir nous centralisons également la connaissance du message d’erreur et de la façon de tester sa présence.

Note : Nous tirons parti de RSpec qui va appeler une méthode has_...? lorsque nous utilisons le matcher expect().to have_.... Cela nous évite d’écrire de nouveaux matchers tout en gardant une bonne lisibilité.

Comment utiliser un Page Object ?

Avant de pouvoir appeler le Page Object dans notre spec, il faut indiquer sa présence à RSpec. Habituellement, nous plaçons nos Page Objects dans le dossier spec/support/pages, notre SignInPage serait donc dans le fichier spec/support/pages/sign_in_page.rb.

# spec/spec_helper.rb
Dir[Rails.root + 'spec/support/pages/*.rb'].each { |f| require f }

Maintenant que RSpec a connaissance des Page Objects il est temps de les intégrer dans la spec !

RSpec.feature 'User sign in form' do
  before(:all) { FactoryGirl.create(:user, email: '[email protected]') }

  let(:sign_in_page) { SignInPage.new }

  scenario 'User signs in successfully' do
    sign_in_page.visit.fill_in_form_correctly('[email protected]')

    expect(current_path).to eq '/'
    expect(page).to have_content 'Successfully signed in !'
  end

  scenario 'User fails to sign in' do
    sign_in_page.visit.fill_in_form_with_errors('[email protected]')

    expect(current_path).to eq sign_in_page.path
    expect(sign_in_page).to have_an_error_message
  end
end

Nous avons déjà grandement simplifié le code de nos scénarii. Un changement important à noter est l’ajout de la ligne suivante, qui crée notre Page :

let(:sign_in_page) { SignInPage.new }

Il reste cependant certaines notions en dur, ces informations sont relatives à la page d’accueil. Il nous faut donc créer un Page Object pour celle-ci.

class HomePage
  def path
    '/'
  end

  def visit
    super path
    self
  end

  def has_a_successful_sign_in_message?
    has_content? 'Successfully signed in !'
  end
end

Nous pouvons finaliser notre spec :

RSpec.feature 'User sign in form' do
  before(:all) { FactoryGirl.create(:user, email: '[email protected]') }

  let(:homepage) { HomePage.new }
  let(:sign_in_page) { SignInPage.new }

  scenario 'User signs in successfully' do
    sign_in_page.visit.fill_in_form_correctly('[email protected]')

    expect(current_path).to eq homepage.path
    expect(homepage).to have_a_successful_sign_in_message
  end

  scenario 'User fails to sign in' do
    sign_in_page.visit.fill_in_form_with_errors('[email protected]')

    expect(current_path).to eq sign_in_page.path
    expect(sign_in_page).to have_an_error_message
  end
end

Aller plus loin

Nous avons déjà fait un grand pas dans la simplification de nos specs mais il reste toutefois un certain nombre de points à améliorer.

Routes nommées de Rails

En particulier les URLs qui sont saisies en dur dans le code alors qu’elles pourraient être directement issues des routes nommées de Rails. C’est en fait assez simple à mettre en place :

class SignInPage
  include Capybara::DSL
  include Rails.application.routes.url_helpers

  def path
    new_user_session_path
  end

  # ...
end

Duplications

D’un Page Object à l’autre, certains éléments sont les mêmes : la méthode visit et l’inclusion de Capybara et autres helpers. Il serait donc préférable de définir une classe SitePage regroupant ces différents aspects.

Voici notre implémentation de cette classe :

class SitePage
  include ActionView::RecordIdentifier
  include Capybara::DSL
  include Rails.application.routes.url_helpers
  include RailsAdmin::Engine.routes.url_helpers

  def id_for(model)
    '#' + dom_id(model)
  end

  def class_for(model)
    dom_class(model)
  end

  def visit
    super path
    self
  end
end

Conclusion

Les Page Objects permettent de simplifier les specs tout en leur apportant une meilleure lisibilité. Traiter avec un seul niveau d’abstraction à la fois rend le code plus flexible et plus facile à maintenir.

Et vous ? Qu’utilisez-vous pour simplifier vos specs ?

Publié le 30 novembre 2014

Notre vision des choses vous correspond ? Vous avez envie de travailler avec nous ?