24/05/2012

Les principales causes d'échec du BDD

Bonjour à tous ! Alors bien évidemment, il serait absurde de vouloir lister toute les erreurs possibles, et totalement illusoire de croire que j'en n'en fait plus ; mais je crois pouvoir donner quelques exemples de ce qu'il faut éviter à tout prix lorsque l'on fait du développement piloté par le comportement avec Behat.

Petit rappel : Behat, c'est quoi ? En un mot, c'est un outil qui va vous permettre de pratiquer du BDD (Behavior Driven Development) en PHP. En d'autres mots, il va vous permettre de tester automatiquement si le développement d'un produit correspond aux spécifications qu'en a donné le client. Vous trouverez une description beaucoup plus complète ici.

Bref, c'est génial, c'est simple à utiliser... mais c'est extrêmement difficile à utiliser correctement. Et c'est catastrophique si c'est mal mal utilisé ! Pourquoi ?

Behat a deux côtés : un côté fonctionnel (rédacteur), rédigé avec la syntaxe de Gherkin, un côté "développeur", en PHP.

De ce que je vois, le plus souvent le fonctionnel (au choix) :

  • ne dispose pas du temps nécessaire pour se consacrer à la rédaction des tests d'acceptation (fonctionnalité)
  • n'a pas une vision assez clair de son produit pour pouvoir le découper fonctionnellement
  • confond interface (ergonomie, disposition...) et comportement de l'application
  • confond contrôle sur les données et test sur le comportement (trèèès souvent!)
  • ou, plus rarement, confond socle technique et fonctionnalité

De ce que je constate, le développeur (au choix) :

  • est obligé de se substituer au client dans la rédaction des tests, ce qui n'est pas son métier (pas simple donc)
  • n'arrive pas à s'abstraire du technique
  • se focalise sur le cheminement (comment arriver là?) et l'emplacement dans l'application
  • a tendance à écrire du code PHP plutôt que de réutiliser des étapes existantes

Bon, le constant est sévère, mais je généralise bien sûr. Cependant il est très difficile d'échapper à ça.

Je passe aux exemples, tirés d'un code vu ce matin même.

Décrire une fonctionnalité : pas si simple

Prenons ce bout de fonctionnalité que j'ai reçu comme spécification :

Feature: access to the task's page from a list of tasks
In order to see a task
As a logged in user
I need to open a task

Background:
Given I am logged in user

Scenario Outline:
When I press "Find a task"
And I fill in "Task reference" with "<reference>"
And I press "Search"
Then I should be on "index/task/id/<id>"

Examples:
| reference   | id  |
| task1       | 1   |
| task2       | 2   |

Bon, ça marche. Mais quand on y regarde plus près :

  • on se consacre plus aux étapes permettant d'accéder aux conditions du scénario qu'au scénario lui-même
  • si la structure de la page change, le test est obsolète
  • on ne teste pas le comportement, mais la donnée. Si la donnée change, le test est obsolète
  • si l'url change (rewriting, etc), le test est obsolète

Bref, le test va rapidement devenir obsolète.

Il est difficile dans ce cas de voir comment s'abstraire des données (liaison id et task). Après réflexion, on peut suggérer d'évoluer vers ceci :

Feature: access to the task's page from a list of tasks
In order to see a task
As a logged in user
I need to open an task's page from a list

Background:
Given I am logged in user

Scenario Outline:
Given I see a list of tasks, including the task "<reference>"
When I follow "<reference>"
Then I should be on the Task's page
And I should see the task "<reference>"

Examples:
| reference  |
| task1      |
| task2      |

On a donc opéré des modifications afin de rendre le test indépendant du jeu de données ou de l'interface de l'application.

Ce que je dis souvent, c'est que, en théorie, un test de comportement est valide quelque soit le support : que l'on passe d'un site web à une application mobile, le changement de support ne change pas la fonctionnalité ou les scénarios ! Ca ne change que leur implémentation.

On pourra certainement trouver encore à redire, mais la fonctionnalité, telle qu'elle est décrite, est désormais valable quelque soit son implémentation technique. Seul son comportement est ici spécifié. Elle a donc une forte probabilité d'être viable et pertinente dans le temps.

Implémenter une définition de fonctionnalité : pas plus facile

On a vu un exemple de fonctionnalité à risque. Passons de l'autre côté et mettons-nous du point de vue du développeur. De la même façon, voici une implémentation possible :

Note : en l’occurrence, la liste des tâches n'est possible dans l'application qu'après  avoir effectué une recherche.

/**
 * @Given /^I see a list of tasks, including the task "([^"]*)"$/
 */
public function iSeeAListOfTasksIncludingTheTask($reference)
{
    $session = $this->getMainContext()->getSubcontext('mink')->getSession();
    $page = $session->getPage();

    $session->visit('/task');

    // Find the task in the search engine
    $page->find('css', '.ipt-task-search')->setValue($reference);
    $page->find('css', '.button-search')->press();

    if ($session->getCurrentUrl() != "/task/{$reference}") {
        throw new AssertException("We cannot find the task {$reference} with the search engine");
    }
}

On constate différente choses :

  • Que de code ! C'est long à écrire
  • Que de code ! Et pas réutilisable en plus !
  • Que de code ! Et qu'est-ce qui se passe si l'interface HTML change ?
  • On ne comprend pas ce qui se passe au premier coup d'oeil

L'implémentation fonctionne, mais est peu viable dans le temps, et surtout on a perdu du temps pour l'écrire (c'est fastidieux de devoir manipuler le navigateur à la main).

Là où Behat est fort, c'est qu'il nous permet, en PHP, de faire comme si on écrivait des étapes de scénario "à la main". Ca fait gagner un temps monstre et permet de réutiliser les définitions existantes :

/**
* @Given /^I see a list of tasks, including the task "([^"]*)"$/
*/
public function iSeeAListOfTasksIncludingTheTask($reference) {
    return array(
        new Given('I am on "/"')
        , new When(sprintf('I fill "Task reference" with "%s"', $reference))
        , new When('I press "Search"')
    );
}

Pour s'aider, le développeur peut s'appuyer (si tout se passe bien) sur la personne qui a rédigé le scénario, qui l'aidera à découper sa définition en différentes étapes.

Attention, contrairement au scénario Gherkin, ce code peut être amené parfois à évoluer. Par  exemple, si on ajoute un scénario pour la recherche de tâche, avec cette étape :

When I search the task ""

On pourra dès lors écrire :

/**
* @Given /^I search the task "([^"]*)"$/
*/
public function iSearchTheTask($reference) {
    return array(
        new Given('I am on "/"')
        , new When(sprintf('I fill "Task reference" with "%s"', $reference))
        , new When('I press "Search"')
    );
}

/**
* @Given /^I see a list of tasks, including the task "([^"]*)"$/
*/
public function iSeeAListOfTasksIncludingTheTask($reference) {
    return array(
        new When(sprintf('I search the task "%s"', $reference))
    );
}

Le développeur peut donc, sans architecture ou code complexe, mais simplement en utilisant les objets Etapes fournis par Behat, organiser ses définitions de façon à les rendre réutilisables.

Le mot de la fin

Bref, ça semble évident, mais quand on fait du BDD... et bien il faut se focaliser le le Comportement. Ce n'est pas facile, et contre intuitif pour beaucoup de monde. Toutefois, le rédacteur du test peut s'aider de la structure de la fonctionnalité (comment la décrire, qui y participe, quels en sont les bénéfices, puis quels cas d'utilisation je peux en donner).

En d'autres mots, le "rédacteur" ne doit pas s'appuyer sur ce qu'il connaît de son application (emplacement, design...), mais sur la vision du produit (comment ça se passe ? Avec quel gain pour l'utilisateur ?). En clair :

Le rédacteur ne doit pas s'appuyer sur ce qu'il connaît de son application mais sur la vision du produit

Le développeur, lui, doit prendre l'habitude de ne pas se lancer tête baissée dans le code, au risque de consacrer trop de temps et d'énergie à l'utilisation de Behat. Certes il doit écrire ce code, mais il ne code plus pour interagir avec un autre code (comme lorsqu'il le fait pour un test unitaire par exemple), mais pour interagir avec un produit. En clair :

Le développeur n'interagit plus avec du code mais avec un produit

Ceci dit, félicitations d'avoir lu ce billet jusqu'au bout :-) .

Je ne prétend pas avoir le recul suffisant, mais je crois que ces constats s'appliquent généralement. C'est le cas pour vous aussi ? Vous avez vu d'autres écueils courants ? Ou au contraire, pour vous tout a roulé tout de suite ?

blog comments powered by Disqus