Kevin Lacointe's blog

And just to be nice, here is an English version of this tutorial...

Introduction

This tutorial will guide you through installing and configuring an acceptance test environment for PHP project.
For this we use several ruby gems, including cucumber, webrat and selenium-client, and write our scripts as cucumber features.
Here's an example:

Feature: Example Features

  Scenario: I am connected and i visit my page
    Given I am connected as "username password"
    When I visit "home page"
    Then I should see "Hello username"

The advantage of this syntax is that it remains very readable for the mere mortals, no need to delve into the ruby code to understand what the test is supposed to do.

Each "sentence" in this scenario refers to a step which is the user actions necessary to perform that action in the user interface.

The user actions can be triggered by the webrat and selenium-client libraries. Webrat is particularly suited to interactions not involving javascript while selenium-client will interpret them correctly.

Installation

For this example we will use a debian system.

Let's start by installing rubygems and the needed gems.

aptitude install rubygems
gem install rspec cucumber webrat selenium-client

To run the binaries packaged with gems from the command line you must add the path to these binaries to your PATH:

export PATH=$PATH:$HOME/.gem/ruby/1.8/bin

To use selenium, start a selenium-rc server (ie: remote control). Starting selenium server is done automatically by webrat, however the version used caused me many problems with recent versions of Firefox. I suggest you grab the latest stable version at the following address:

http://seleniumhq.org/download/ (1.0.3 au 28 juin 2010).

It is sufficient to replace the file ~/.gem/ruby/1.8/gems/webrat-0.7.1/vendor/selenium-server.jar with the version you just downloaded.

wget http://selenium.googlecode.com/files/selenium-remote-control-1.0.3.zip
unzip selenium-remote-control-1.0.3.zip
cp selenium-server-1.0.3/selenium-server.jar $GEM_HOME/webrat-0.7.1/vendor/selenium-server.jar

(Remplace $GEM_HOME by your gems installation directory.)

Voila, now we can start writing our scenarios.

Note for iceweasel: you must add the path to the folder containing the iceweasel application.ini file to your PATH otherwise

iceweasel will not start.

export PATH=$PATH:/usr/lib/iceweasel

Configuration

Now that the installation is complete, we will have to configure a web environment to run our tests.
This environment can be your development environment, the approach is specific to each project so I will not dwell on this point.

Let's turn to the configuration of our test environment.
We will create a directory features in the root of your project. This directory has the following structure:

$: features % tree
.
├── fixtures
│   └── image.jpg
├── home.feature
├── step_definitions
│   ├── user_steps.rb
│   └── webrat_steps.rb
└── support
    ├── env.rb
    ├── paths.rb
    └── selenium.rb

3 directories, 10 files

Let us see more precisely what role each of these directories is.

The root of the features will contain your cucumber "features", ie scenarios.

fixtures contains the files used in your scripts (eg an image to upload to a file field).

The step_definitions stores definitions of steps used in the scenarios. It will usually contain a file per action type, in our example we have two files:

  • webrat_steps: Common actions, this file can be found in the code of the webrat gem (templates/skeleton/step_definitions/webrat_steps.rb.erb). It contains, among other common helpers to any type of web project (visit a page, check that it contains a text or an html tag, ...)
  • user_steps : the user actions (login, post a comment ...)

The support is the configuration directory, it must contain the following files:

  • env.rb, the webrat configuration file
  • path.rb, is the file that defines the correspondence between the named routes that you call and the actual URL to send to the browser (example: "home page" => "/")
  • selenium.rb, is the file where you can refactor out the specific actions to selenium.

Webrat

By default webrat does not use selenium to control the browser but uses instead mechanize, here is a sample configuration file to be placed in the support/env.rb to specify the use of selenium:
(Note in passing that the address of your test web environment will be configured via an environment variable for simplicities sake):

unless ENV['CUCUMBER_HOST']
  raise 'You must set CUCUMBER_HOST environment variable with the name of your host used to run cucumber features'
end

# RSpec
require 'spec/expectations'

# Webrat
require 'webrat'

require 'test/unit/assertions'
World(Test::Unit::Assertions)

Webrat.configure do |config|
  #config.mode = :mechanize
  config.mode = :selenium
  config.application_framework = :external
  config.application_address = ENV['CUCUMBER_HOST']
  config.application_port = "80"
end

World do
  session = Webrat::Session.new
  session.extend(Webrat::Methods)
  session.extend(Webrat::Matchers)
  session
end

Named Routes

Next you must specify the routes that can be called in your tests and their text versions. Indeed, in cucumber scenarios , a literary correspondence will be much more readable than the raw URL. Here is an example configuration of these routes to be stored in the file support/path.rb :

module NavigationHelpers
  def path_to(page_name)
    case page_name
    when /home/
      '/'
    when /accueil/
      '/accueil'
    when /login/
      '/identification'
    else
      raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
        "Now, go and add a mapping in #{__FILE__}"
    end
  end
end

World(NavigationHelpers)

Helpers

In order to have the scenarios as clear and as understandable as possible, we will define some helpers.
All files in the support will be loaded by cucumber in our example we will add a file selenium.rb for actions related to selenium.

Here is the support/selenium.rb file:

def wait_for_ajax(timeout=50000, request_count = 1)
  request_count.times do |index|
    selenium.wait_for_ajax :javascript_framework => :jquery
  end 
end
def wait_for_gritter
  selenium.wait_for_element "gritter-notice-wrapper"
end

You can do the same for other libraries you might need.

Writing scripts

The scenarios are defined by several cucumber keywords: Given, When, Then and And.

  • Given defines the initial context
  • When defines user actions
  • Then defines an assertion, the expected result
  • And is an association word to make the scenatio more readable, it has the same role as the keyword of the previous line.

Some actions will be called in several scenarios, hence the need for code refactoring.
For example, the action Given I am connected as "username password" will certainly be present in 75% of your tests.
To avoid repeating the X actions necessary to connect, we refactored that in the steps of the step_definitions directory.

Here is an example of steps for the following example:

Given /^I am connected as "(\w)\ (\w)"$/ do |login, password|
  visit path_to "login"
  fill_in 'login', :with =>  login
  fill_in 'password', :with => password
  click_button "login"
  wait_for_ajax # if your form is submitted via ajax
end 

Now in each of our tests we can call: Given I am connected as "MyUsername MyPassword".

Feature: Example Features

  Scenario: I am connected and i visit my page
    Given I am connected as "username password"
    When I visit "home page"
    Then I should see "Hello username"

Run !

Now that you've written your first scenario, go to the root of your project and run the following command:

CUCUMBER_HOST=YOUR_HOST cucumber

(Replace YOUR_HOST by your test enviroment address.)

You should see your browser run your script ... hands free!

Introduction

Ce tutoriel va vous guider dans l'installation et la configuration d'un environnement de test d'acceptance sur un projet PHP. Pour cela nous utiliserons plusieurs gems ruby, notamment cucumber, webrat et selenium-client, et écrirons nos scénarios sous forme de features cucumber. Voici un exemple :

Feature: Example Features

Scenario: I am connected and i visit my page Given I am connected as "username password" When I visit "home page" Then I should see "Hello username"

L'avantage de cette syntaxe est qu'elle reste très lisible pour le communs des mortels, pas besoin de mettre le nez dans le code ruby pour comprendre ce que le test est sensé faire.

Chaque "phrase" de ce scénario fait référence à des "steps" (étapes) qui sont en réalité les actions utilisateurs nécessaires pour effectuer l'action correspondante dans l'interface utilisateur.

Les actions utilisateurs peuvent être déclenchés par les librairies webrat et selenium-client. Webrat est plus particulièrement adapté aux interactions ne faisant pas intervenir de javascript alors que selenium-client saura les interpréter correctement.

Installation

Pour cet exemple nous utiliserons un système debian.

Commençons par installer rubygems et les gems nécessaires.

aptitude install rubygems
gem install rspec cucumber webrat selenium-client

Pour pouvoir exécuter les binaires packagés avec les gems en ligne de commande vous devrez ajouter le chemin vers ces binaires à votre PATH :

export PATH=$PATH:$HOME/.gem/ruby/1.8/bin

Pour pouvoir utiliser selenium, il faut démarrer un serveur selenium-rc (ie: remote control). Le démarrage du serveur selenium est fait automatiquement par webrat, en revanche la version utilisée m'a posé plusieurs problèmes avec les versions récentes de firefox. Je vous conseille donc de récupérer la dernière version stable à l'adresse suivante : http://seleniumhq.org/download/ (1.0.3 au 28 juin 2010). Il suffira ensuite de remplacer le fichier ~/.gem/ruby/1.8/gems/webrat-0.7.1/vendor/selenium-server.jar par la version que vous venez de télécharger.

wget http://selenium.googlecode.com/files/selenium-remote-control-1.0.3.zip
unzip selenium-remote-control-1.0.3.zip
cp selenium-server-1.0.3/selenium-server.jar $GEM_HOME/webrat-0.7.1/vendor/selenium-server.jar

(Remplacez $GEM_HOME par le répertoire d'installation de vos gems.)

Voila, nous allons pouvoir écrire nos scénarios.

Note pour iceweasel : vous devrez ajouter le chemin vers le dossier contenant le fichier application.ini de iceweasel à votre PATH, sinon iceweasel ne démarrera pas.

export PATH=$PATH:/usr/lib/iceweasel

Configuration

Maintenant que l'installation est terminée, il va falloir configurer un environnement web pour lancer nos tests. Cet environnement peut être votre environnement de développement, la démarche est spécifique à chaque projet donc je ne m'attarderai pas sur ce point.

Passons maintenant à la configuration de notre environnement de test. Créons un répertoire features à la racine de votre projet. Ce répertoire aura la structure suivante :

$: features % tree
.
├── fixtures
│   └── image.jpg
├── home.feature
├── step_definitions
│   ├── user_steps.rb
│   └── webrat_steps.rb
└── support
    ├── env.rb
    ├── paths.rb
    └── selenium.rb

3 directories, 10 files

Voyons plus précisément quel est le rôle de chacun de ces répertoires.

Le racine du répertoire features contiendra vos "features" cucumber, c'est à dire les scénarios.

Le répertoire fixtures contiendra les fichiers utilisés dans vos scénarios (par exemple une image à envoyer dans un champs file).

Le répertoire step_definitions stocke les définitions des étapes utilisées dans les scénarios. Il contiendra généralement un fichier par type d'action, dans notre exemple nous aurons deux fichiers :

  • webrat_steps : les actions communes, ce fichier peut être trouvé dans le code du gem webrat (templates/skeleton/step_definitions/webrat_steps.rb.erb). Il contient, entre autre, les helpers communs à tout type de projet web (visiter une page, vérifier qu'elle contient un texte ou une balise, ...)
  • user_steps : les actions utilisateurs (se connecter, poster un commentaire...)

Le répertoire support est le répertoire de configuration, il doit contenir les fichiers suivants :

  • env.rb, le fichier de configuration de webrat
  • path.rb, c'est le fichier qui définit la correspondance entre les routes nommées que vous appellerez et les véritables URL à envoyer au navigateur (par exemple : "home page" => "/")
  • selenium.rb, c'est le fichier où vous pourrez factoriser les actions spécifiques à selenium.

Webrat

Par défaut webrat n'utilise pas selenium pour controler le navigateur mais mechanize, voici un exemple de configuration à placer dans le fichier support/env.rb pour spécifier l'utilisation de selenium (on notera au passage que l'adresse de votre environnement web de test sera configurée via une variable d'environnement pour plus de simplicité) :

unless ENV['CUCUMBER_HOST']
  raise 'You must set CUCUMBER_HOST environment variable with the name of your host used to run cucumber features'
end

RSpec

require 'spec/expectations'

Webrat

require 'webrat'

require 'test/unit/assertions' World(Test::Unit::Assertions)

Webrat.configure do |config| #config.mode = :mechanize config.mode = :selenium config.application_framework = :external config.application_address = ENV['CUCUMBER_HOST'] config.application_port = "80" end

World do session = Webrat::Session.new session.extend(Webrat::Methods) session.extend(Webrat::Matchers) session end

Routes nommées

Ensuite vous devez spécifier les routes pouvant être appelées dans vos tests ainsi que leurs versions texte. En effet, dans les scénarios cucumber, une correspondance littéraire sera beaucoup plus lisible que les URL brutes. Voici un exemple de configuration de ces routes qui seront stockées dans le fichier support/path.rb :

module NavigationHelpers
  def path_to(page_name)
    case page_name
    when /home/
      '/'
    when /accueil/
      '/accueil'
    when /login/
      '/identification'
    else
      raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
        "Now, go and add a mapping in #{FILE}"
    end
  end
end

World(NavigationHelpers)

Helpers

Dans le but d'avoir des scénarios les plus clairs et compréhensibles possibles, nous allons définir quelques helpers. Tous les fichiers présents dans le répertoire support seront chargés par cucumber, dans notre exemple nous allons ajouter un fichier selenium.rb pour les actions relatives à selenium.

Voici donc le fichier support/selenium.rb :

def wait_for_ajax(timeout=50000, request_count = 1)
  request_count.times do |index|
    selenium.wait_for_ajax :javascript_framework => :jquery
  end 
end
def wait_for_gritter
  selenium.wait_for_element "gritter-notice-wrapper"
end

Vous pourrez faire de même pour les autres librairies dont vous pourriez avoir besoin.

Écrire des scénarios

Les scénarios cucumber sont définis par plusieurs mots-clés : Given, When, Then et And.

  • Given définit le contexte de départ.
  • When définit une action utilisateur.
  • Then définit une assertion, le résultat à obtenir.
  • And est un mot de liaison permettant de rendre encore plus lisible le scénario, il a le même rôle que le mot-clé de la ligne précédente.

Certaines d'actions seront appelées dans plusieurs scénarios, d'où le besoin de factorisation de ce code. Par exemple, l'action Given I am connected as "username password" sera très certainement présentes dans 75% de vos tests. Pour éviter de répéter les X actions nécessaires pour se connecter, nous allons factoriser cela dans les étapes du répertoire step_definitions.

Voici un exemple d'étapes correspondant à l'exemple suivant :

Given /^I am connected as "(\w)\ (\w)"$/ do |login, password|
  visit path_to "login"
  fill_in 'login', :with =>  login
  fill_in 'password', :with => password
  click_button "login"
  wait_for_ajax # if your form is submitted via ajax
end 

Désormais dans chacun de nos test nous pourront appeler : Given I am connected as "MyUsername MyPassword".

Feature: Example Features

Scenario: I am connected and i visit my page Given I am connected as "username password" When I visit "home page" Then I should see "Hello username"

Run !

Maintenant que vous avez écrit votre premier scénario, placez-vous à la racine de votre projet et lancer la commande suivante :

CUCUMBER_HOST=YOUR_HOST cucumber

(Remplacez YOUR_HOST par l'adresse de votre environnement de test.)

Vous devriez voir votre navigateur réaliser votre scénario... sans les mains !

AF83 sort une gem que nous utilisons dans nos projets de sites communautaires. Has_media est une bibliothèque pour ActiveRecord, pour gérer les médias dans les modèles avec une simple déclaration :


class User < ActiveRecord::Base
has_one_medium :avatar, :only => :image
end

On ajoute avec cette ligne un "champ" image pour un utilisateur. Il ne reste plus qu'à ajouter un file_field :avatar dans votre formulaire pour que l'image soit associée à l'utilisateur. L'upload est géré par carrierwave. Les méthodes de classe has_one_medium ou has_many_media ajoutent automatiquement les getter et setter pour l'avatar d'un utilisateur.

Les validations sont faites automatiquement selon le type de média défini par l'option :only => :image.

Cas d'utilisation :

# Créer la migration
./script/generate has_media


# Ajouter la gem dans config/environment.rb
config.gem 'has_media'


# Changer les options de has_media dans config/initializers/has_media.rb
HasMedia.directory_path = "media" # Placer les médias dans Rails.root, 'public', 'media'
HasMedia.directory_uri = "/media"
HasMedia.errors_messages = {:type_error => I18n.t('has_media.errors.type_error')}


# Pour le modèle voir plus haut et sur le formulaire
<p>
<%= f.label :avatar %>
<%= f.file_field :avatar %>
</p>
</code>

HasMedia ne gère pas les thumbnails (AF83 utilise une autre application pour cela), mais c'est facilement faisable avec carrierwave.

Plus d'informations : http://github.com/AF83/has_media
Installation : gem install has_media