June 2010

Voici de nouveaux événements qui viennent compléter la précédente liste.

1er juillet : Drumbeat Paris - http://www.drumbeat.org/events/drumbeat-paris
Est-ce que le Web sera ouvert dans dix, vingt – voire dans 50 ans ? Mozilla pense qu'il peut – et doit – l'être. C'est pourquoi nous lançons l'initiative Drumbeat, une invitation à tout passionné du Web à participer à des projets qui améliorent le Web et le rendent plus ouvert à long terme.

21 juillet : WebPerf User Group - https://sites.google.com/a/survol.fr/webperf-user-group/evenements/21-ju...
Le WebPerf User Group une réunion informelle où tout le monde est le bienvenu, quel que soit son rôle ou sa connaissance du sujet, pour discuter et échanger à propos des performances web.

19 août : Whyday - http://whyday.org/
Le 19 août sera l'occasion de célébrer le Whyday, en l'honneur de Why the lucky stiff. Ce hacker a très fortement influencé la communauté Ruby, par ses écrits et notamment le guide poignant de why, ses projets Ruby (Camping, Shoes, RedCloth, Try Ruby, etc.), et plus généralement par sa présence en ligne. Le 19 août de l'année dernière, il a décidé de supprimer toute son activité en ligne, probablement pour préserver son anonymat. Le Why Day sera l'occasion de lui rendre hommage et de se lancer dans des projets fous.

28 et 29 août : Node.js Knockout - http://nodeknockout.com/
Le node.js knockout est une compétition qui consiste à construire un site web avec node.js en moins de 48h. Les équipes d'au plus 4 personnes seront jugées, et les sites les plus impressionnants récompensés.

15 au 17 octobre : Rails Rumble - http://blog.railsrumble.com/
Le Rails Rumble a les mêmes règles que le Node.js knockout, mais pour Rails à la place de Node.js. Ce n'est pas étonnant car les précédentes éditions du Rails Rumble ont inspirées le Node.js Knockout.

9 et 10 novembre : Forum PHP - http://afup.org/pages/forumphp2010/
Le Forum PHP est la grand messe française autour de PHP. Il sera placé sous le signe des 15 ans de PHP et des 10 ans de l'Afup. A cette occasion, l'Afup organise un Forum plus ambitieux que jamais, prévoyant de multiples conférences, des ateliers et débats, des invités renommés, mais aussi des espaces d'intervention et d'exposition plus nombreux !

12 novembre : fullfrontal - http://2010.full-frontal.org/
Fullfrontal est une conférence javascript qui promet d'être bien sympathique.

One Ring to rule them all, One Ring to find them, One Ring to bring them all and in the darkness bind them.

Here the "them" doesn't refer to other rings, but to Javascript modules/packages, server side or browser/client side. This article is about running the same code on nodejs (server) and on the browser (client), without having to change nodejs module codes.

If you know Javascript, then you already know it is a good language to express asynchronous treatments. The Javascript language have other strong points, but weak points as well. One of the often quoted advantage of JS, is that it can run on both server and client. You can then share your code as needed, which avoid you the pain to have two code bases to do the same thing (like forms validation) : once on the browser, once on the server.

My goal here is to be able to run my server side JS (written for nodejs) on the browser, without having to change the sources manually nor passing by a "compilation / transformation" process I would have to run before testing on the browser side. Of course I don't want to start a TCP server from the browser, but I'd like to be able to reuse general purpose libraries at ease.

The first problem comes from the fact that there is no isolation (or namespaces) between different modules on browser side, so if there are not wrapped in a function, then you'll end-up polluting your global namespace. The second problem comes from the fact that "require" and "exports" statements are not standard JS, but a proposal from CommonJS, and so won't be defined in the browser by default. Luckily, James Brantly wrote Yabble - Yet Another (CommonJS) Browser Loader, which can solve these two problems (Thanks James!). Here is how we can use it:

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr" id="html">
  3.   <head>
  4.     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  5.     <title>Yabble demo</title>
  6.  
  7.     <script type="text/javascript" src="/yabble.js"></script>
  8.     <script type="text/javascript">
  9.       require.reset();
  10.  
  11.       require.useScriptTags(); // to avoid using XHR
  12.       require.setModuleRoot('wrapped_js/');
  13.  
  14.       require.ensure(['A','B'], function() {
  15.         require.run('A');
  16.       });
  17.   </head>
  18.  
  19.   <body>
  20.     <h1>Yabble demo</h1>
  21.   </body>
  22. </html>

Your server has to be able to serve the following JS files: "/wrapped_js/A.js" and "/wrapped_js/B.js". Yabble will fetch those two JS modules and then run A (which can require B, since it has been loaded). The fine part with the "require.ensure", is that it will load JS modules in parallel.

By default, Yabble uses XHR requests to load the JS modules, and then eval them. This is not the most convenient way of using it since you won't be able to debug them. By writting require.useScriptTags(); we ask Yabble to retrieve the module as scripts files; but the modules code then need to be wrapped as:

  1. require.define({'B': function(require, exports, module) {
  2.   // code module B
  3. }}, ['A']); // B depends of A

Here we say that 'B' depends of 'A', so Yabble will ensure A has been loaded before running B. If we know for sure all dependencies are loaded, we don't have to specify them though.

Yabble features a tool that can wrap the modules for us, but it's pretty static (and I don't want to run it every time I change my sources). So I wrote some code (part of nodetk) that automatically serve JS files: you just have to specify which modules / packages / static files you want to serve, and it will do so. It also wraps automatically JS module files on the fly.

Example of use (on server side):

  1. var http = require("http"),
  2.     bserver = require('nodetk/browser/server');
  3.  
  4. // Here we create a http server...
  5. var server = http.createServer();
  6. // ... and do whatever we want with it
  7.  
  8. // we use that previous server to serve JS modules
  9. bserver.serve_modules(server, {
  10.   modules: ['assert', 'sys'],
  11.   packages: ['nodetk', 'rest-mongo'],
  12.   additional_files: {
  13.     '/tests.html': __dirname + '/tests.html',
  14.     '/tests.js': __dirname + '/tests.js'
  15.   }
  16. })
  17.  
  18. server.listen(8080)

Now with this code, on client side, we can do any of the followings:

  1. var sys = require('sys'),
  2.     assert = require('assert'),
  3.     utils = require('nodetk/utils'),
  4.     callbacks = require('nodetk/orchestration/callbacks'),
  5.     rest = require('rest-mongo/core');

nodetk also features a javascript file to initialize some variables such as __dirname or process.

For a complete working example, I encourage you to have a look at nodetk tests, which are running on both server and client side (except for modules which can not run on browser): http://github.com/AF83/nodetk/tree/master/src/nodetk/browser_tests/.

Pierre

Le 17 juin à La Cantine était organisée une soirée autour de l'apport de l'ergonomie lors de la conception d'un produit. La soirée était organisée par le Réseau des jeunes chercheurs en ergonomie, réseau qui compte une quarantaine d'ergonomes et de jeunes doctorants.

Bonjour à tous,

Voici, en vrac, les liens de veille de la semaine (dernière) :

Bonne journée,
Pierre

Vous avez peur de vous ennuyer cet été ? Alors, voici un petit programme pour vous occuper :

21 juin : MongoFR à la Cantine - http://www.10gen.com/conferences/event_mongofr_21june10
Cette journée organisée par 10gen et sponsorisée par af83 sera entièrement consacrée à MongoDB.

26 juin : Social Innovation BarCamp Paris à la Cantine - http://barcamp.org/SocialInnovationBarcampParis
Ce barcamp autour du thème de l'innovation sociale sera la rencontrer de plein de gens intéressants. Rappelons que l'innovation sociale est toute forme d'innovation économique, organisationnelle ou technologique appliquée à des problématiques à caractère social ou à toute problématique liée aux individus et à la façon dont la société fonctionne ou pourrait être améliorée.

3 juillet : Web Workers Camp, organisé par af83 (Pierre et Louis A.) à la Cantine - http://barcamp.org/WebWorkersCamp
Ce barcamp sera l'occasion de parler de plein de technos cools comme Node.js, les bases NoSQL, les websockets, les files d'attentes, etc. Nous aurons la chance d'accueillir Ryan Dahl, le créateur de Node.js.

Du 6 au 11 juillet : RMLL 2010 à Bordeaux - http://2010.rmll.info/
Les RMLL 2010 sont un cycle non commercial et d’accès gratuit de conférences, ateliers et tables rondes autour du Logiciel Libre et de ses usages. L’objectif est de fournir un lieu d’échanges et de rencontres entre utilisateurs, développeurs et acteurs du Logiciel Libre. N'oubliez pas de venir à ma conférence sur Ruby 1.9 ;-)

Tout l'été : NoSQL Summer Paris - http://nosqlsummer.org/city/paris
Le NoSQL Summer Paris est l'occasion de découvrir des papiers sur les bases de données, relationnelles ou non, et d'en discuter avec d'autres dévs.

28 et 29 août : Pycon Fr 2010 - http://zope.afpy.org/Members/jpcw2002/pycon2010news
L'Association Francophone Python organise comme à son habitude son rendez-vous annuel autour de Python : Pycon FR. Venez échanger sur Python avec des passionnés à la Cyberbase de la Villette à Paris les 28 et 29 août. Des dizaines de conférences, courtes présentations et tutoriels seront égrainés tout au long du week-end, sur des sujets tout public ou parfois un peu plus techniques.

25 et 26 septembre : JSConf.eu à Berlin - http://jsconf.eu/2010/
La JSConf.eu est l'événement européen autour du javascript (coté client et coté serveur) à ne pas manquer. Vous pouvez encore répondre au call for speakers, mais il ne vous reste plus beaucoup de temps.

14 au 16 octobre : Paris Web - http://www.paris-web.fr/
La cinquième édition de la conférence Paris Web explorera les thèmes de l’accessibilité Web, du design numérique et des standards ouverts.

Last month, Ori was debugging an IE / Flex URLRequest / SSL bug, and the solution to his problem was a Pragma. Today I was debugging on nodejs, and the origin of my problem was a pragma...

Well, to be precise, the pragma was at the origin of the problem as I encountered it, but it could have been any header field. First things first, let's start by the beginning:

  • I'm writing an application using nodejs, the über asynchronous JS framework by Ryan Dahl.
  • For some reasons, this application collects URLs people post in different places, to have them in one place. Because nodejs make it easy, the application checks the URLs posted are valid and if the server answer by a redirect, keep the result of the redirection.
  • Yesterday's night, Shakaman (the author of ShakaCSS) posted an URL on IRC and here is another corresponding one: http://bonjourlechat.fr, but the link didn't appeared in the collected URLs.
  • I first thought it was the server fault, so I came back home. But this morning, I checked the answer from the server, just to be sure, and it was a regular 301 reply. There was a bug somewhere.

After reproducing the bug I found out that nodejs headers had no "location" header field but a "pragmalocation" one. I first thought "WTF, is that some kind of new header I haven't heard about?!", then looked up on the Internet... To find nothing.

So the "pragmalocation" header field does not exist, but shows up in nodejs... Firebug doesn't tell me anything about any pragma header, but Chromium does: it seems there is a prama header field with an empty value. wget gave me the exact order in which header fields were given:

  1.   HTTP/1.0 301 Moved Permanently
  2.   Date: Thu, 03 Jun 2010 09:56:32 GMT
  3.   Server: Apache/2.2.3 (Red Hat)
  4.   X-Tumblr-Perf: "ch:0/ cm:0/ ce:0/ c:0/0 d:0/0 e:0/0"
  5.   P3P: CP="ALL ADM DEV PSAi COM OUR OTRo STP IND ONL"
  6.   Cache-Control: public
  7.   Pragma: 
  8.   Location: http://www.bonjourlechat.fr/
  9.   Vary: Accept-Encoding
  10.   X-Tumblr-Usec: D=47851
  11.   Content-Length: 0
  12.   Content-Type: text/html; charset=UTF-8
  13.   X-Cache: MISS from rack1.tumblr.com
  14.   X-Cache-Lookup: MISS from rack1.tumblr.com:80
  15.   Via: 1.0 rack1.tumblr.com:80 (squid/2.6.STABLE6)
  16.   Connection: keep-alive

From now it was clear what the cause of the bug was: no value in a header field would provoke the field name to be concatenated to the next one... I was able to reproduce the bug with other header field names.

After further investigation, it was clear that the problem came from the http-parser used by nodejs (http://github.com/ry/http-parser). This parser is a finite state machine designed to be fast and triggering some callbacks along the parsing. So the state machine is basically triggering two callbacks:

  • on_header_field: called when some pieces of header field names is parsed;
  • on_header_value: called when some pieces of header field values is parsed.

The problem with such approach, is that only using the two provided callbacks, it is not possible to detect the case where a header field has no value... It also makes it a bit tricky to use.

So I proposed a solution, which is just to add another callback to be called once the parser finish to parse a header field. The patch is pretty small (3 lines in the parser), but is makes it easier to use and solve our problem. As any bug need a test, I added a test reproducing the bug. The test making a reference to bonjourlechat.fr, this is how this website could enter the nodejs code base!

To illustrate how simple it makes the code using the parser, here is how could be rewritten the http-parser.js :

Before:

  1.   ...
  2.  
  3.   parser.onMessageBegin = function () {
  4.     parser.incoming = new IncomingMessage(parser.socket);
  5.     parser.field = null;
  6.     parser.value = null;
  7.   };
  8.  
  9.   ...
  10.  
  11.   parser.onHeaderField = function (b, start, len) {
  12.     var slice = b.toString('ascii', start, start+len).toLowerCase();
  13.     if (parser.value) {
  14.       parser.incoming._addHeaderLine(parser.field, parser.value);
  15.       parser.field = null;
  16.       parser.value = null;
  17.     }
  18.     if (parser.field) {
  19.       parser.field += slice;
  20.     } else {
  21.       parser.field = slice;
  22.     }
  23.   };
  24.  
  25.   parser.onHeaderValue = function (b, start, len) {
  26.     var slice = b.toString('ascii', start, start+len);
  27.     if (parser.value) {
  28.       parser.value += slice;
  29.     } else {
  30.       parser.value = slice;
  31.     }
  32.   };
  33.  
  34.   ...

After:

  1.   ...
  2.  
  3.   parser.onMessageBegin = function () {
  4.     parser.incoming = new IncomingMessage(parser.socket);
  5.     parser.field = "";
  6.     parser.value = "";
  7.   };
  8.  
  9.   ...
  10.  
  11.   parser.onHeaderField = function (b, start, len) {
  12.     parser.field += b.toString('ascii', start, start+len).toLowerCase();
  13.   };
  14.  
  15.   parser.onHeaderValue = function (b, start, len) {
  16.     parser.value += b.toString('ascii', start, start+len);
  17.   };
  18.  
  19.   parser.onHeaderValueComplete = function () {
  20.     parser.incoming._addHeaderLine(parser.field, parser.value);
  21.     parser.field = "";
  22.     parser.value = "";
  23.   };
  24.  
  25.   ...

This is much simpler! I like the story of this bug, this is why I posted here. Along the debugging process, I had a look at the way JS and C++ communicate in nodejs, and also probably learned some C tricks reading Ryan code, so thanks Ryan!

Pierre

Update: The patch has not been accepted, Ryan preferring to keep the API as it. But the test case has been kept (cf. commit).

Bonjour à tous,

Voici quelques liens de veille, en vrac :

Bon week-end !
Pierre

Af83 a accueilli la rencontre mensuelle du groupe UXParis.

L’invité était Paul Kahn, architecte de l’information.
New-yorkais, tombée très tôt dans le web, il a contribué dans le milieu des années 80 à des groupes de recherches qui travaillaient sur les systèmes hypertextes au sein de Brown University. Il crée un studio de design de l'information à Paris en 2001, tentant de traduire cette terminologie en France.

Il a choisi comme thème de la rencontre : Data : No structure/ Some Structure/ Structure

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 !