Archive by Author

Flux personnalisés et filtres en PHP (Streams)

13 avr

{lang: 'fr'}

Lorsque vous faites un fopen(), ou toute autre fonction équivalente, PHP vous retourne une ressource, sous forme d’un flux. Il existe différent types de flux en PHP :

fopen('file://...') // fichier
fopen('php://temp') // fichier temporaire
fopen('php://memory') // en mémoire
fopen('php://stdout') // sortie de la console
etc.

Bref, il y en a pas mal…

Mais on oublie souvent qu’on peut aussi ajouter ses propres types de flux. Par exemple je vais créer un type de flux « twitter » pour lire mes tweets :

$fp = fopen("twitter://@halleck45", "r");
if ($fp) {
    while (!feof($fp)) {
        var_dump(fgets($fp, 140));
    }
}

Création d’un nouveau type de flux

C’est relativement facile : il suffit d’ajouter un nouveau gestionnaire de flux, c’est à dire une classe qui respecte le prototype StreamWrapper.

stream_wrapper_register("twitter", "TwitterStream");

Plutôt que d’implémenter toutes les méthodes de ce prototype, concentrons nous sur le principal, et créons 4 méthodes :

  • stream_open(), qui va ouvrir notre flux
  • stream_close(), pour le fermer
  • stream_read($size) qui va être appelée à chaque lecture dans le flux
  • stream_eof(), pour indiquer qu’on arrive à la fin

Pour simplifier l’exemple, le flux Twitter est en lecture seule. Pour la même raison, on va rapatrier tous les tweets d’un coup et les stocker dans notre objet dans un tableau.

Pour démarrer, créons une petite fonction qui va aller chercher les tweets d’un utilisateur donné :

ini_set('allow_url_fopen', 1);
define('TWITTER_PWD', 'votre-mot-de-passe');
define('TWITTER_LOGIN', 'votre-login');

function example_stream_twitter_fetch($username) {
    $ch = curl_init();
    $url = sprintf('http://api.twitter.com/1/statuses/user_timeline.json?screen_name=%s&include_entities=true&include_rts=true&count=20', $username);
    curl_setopt($ch, CURLOPT_URL,$url);
    curl_setopt($ch, CURLOPT_USERPWD, TWITTER_LOGIN . ':' . TWITTER_PWD);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $result = curl_exec($ch);
    curl_close($ch);
    return $result ? json_decode($result) : null;
}

Maintenant le gestionnaire de flux :

class TwitterStream {

    protected $_json;
    protected $_offset = 0;

    public function stream_open($path, $mode, $options, &$opened_path) {

        //
        // Read only !
        if ($mode != 'r') {
            trigger_error("Unsupported mode", E_USER_WARNING);
            return false;
        }

        //
        // Calling example_stream_twitter_fetch() in order to fetch data from twitter
        $result = false;
        if (preg_match('!@(\w*)!', $path, $match)) {
            $result = example_stream_twitter_fetch($match[1]);
        }

        if (!$result) {
            if (($options & STREAM_REPORT_ERRORS)) {
                trigger_error("Username not found", E_USER_WARNING);
            }
            return false;
        }

        $this->_json = $result;
        return (bool) $result;
    }

    public function stream_close() {
        $this->_json = null;
        return true;
    }

    public function stream_read($count) {
        return $this->_json[$this->_offset++]->text;
    }

    public function stream_eof() {
        return $this->_offset >= sizeof($this->_json);
    }

}

Rien de bien compliqué. Il faut juste bien penser à lancer un warning en cas de problème…

Et ça suffit :

stream_wrapper_register("twitter", "TwitterStream");

$fp = fopen("twitter://@halleck45", "r", STREAM_REPORT_ERRORS);
if ($fp) {
    while (!feof($fp)) {
        var_dump(fgets($fp, 140));
    }
}

Pratique non ?

Bon, bien sûr l’exemple est trivial, ne serait-ce parce que les tweets peuvent dépasser les 140 caractères à cause des liens ; mais je pense que vous aurez compris l’intérêt de la chose :-)

Appliquer des fitres sur des flux

Autre « truc pratique » assez peu utilisé mais vraiment utile : on peut appliquer des filtre sur des flux, même sur les flux « natifs ».

Par exemple, je veux convertir le texte que j’écris dans un fichier en l33t, en utilisant cette fonction :

function l33t($string) {
    return str_replace(array('l', 'e', 't'), array('1', '3', '7'), $string);
}

Nous allons enregistrer le filtre « l33t » et l’associer à la classe « l33t_filter’ :

stream_filter_register("l33t", "l33t_filter");

Cette classe l33t_filter doit hériter de la classe native php_user_filter :

class l33t_filter extends php_user_filter {

    function filter($in, $out, &$consumed, $closing) {

        while ($bucket = stream_bucket_make_writeable($in)) {
            //
            // leet -> l33t
            $bucket->data = l33t($bucket->data);
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        // on retournerait PSFS_ERR_FATAL en cas d'erreur bloquante
        return PSFS_PASS_ON;
    }
}

On peut par exemple imaginer appliquer des filtres de cryptage, de contrôle…

Alors, convaincu ? :-) Avez-vous déjà utilisé votre propres flux ou filtre de flux en PHP ?

Behat : liens et ressources utiles

2 avr

{lang: 'fr'}

Ce billet fait suite à :

Pour ce dernier billet du tutoriel Behat, j’avais prévu de parler la mise en place de Behat au sein d’une plate-forme d’intégration continue (PIC). Je me suis rendu compte qu’il y avait déjà quelques bons tutos là dessus. Je vous propose donc plutôt une petite liste de liens que je vous invite à découvrir pour aller plus loin avec Behat.

Contextes prêts à l’usage

Le BehatCH, développé par PMSIpilot est ce que j’ai trouvé de plus complet pour apporter des bonnes bases à un projet. regardez surtout le BrowserContext et le TableContext, qui permettent d’écrire des choses comme :

I wait "5" seconds until I see "element"
I should see "5" elements
The 1st column of the 1st row in the "table" table should contain "Lorem"

Vous pouvez également utiliser les Extra Contexts mis à disposition par l’auteur de Behat, qui sont eux moins axés outils mais plus à mon sens comme des composants pour créer d’autres outils.

Blogs intéressants sur Behat

Pour les blogs francophones, je vous recommande les blog de PMSIpilot et de knpLabs (bon, ok c’est plus souvent en anglais qu’en français :-) …).

Pour les anglohpones, vous pouvez jeter un oeil sur le blog de Shashikant Jagtap, de l’auteur même de Behat, ou aussi sur craftItOnline

Plate-forme d’intégration continue et Behat

Avant toute chose sachez que Behat permet très facilement de générer des rapports en XML, au format JUnit :

behat -f junit --out ./report

Voici un tutoriel pour installer Behat sur Jenkins, et en voici un autre pour Hudson

Bonne lecture ! :-) N’hésitez pas si vous avez des ressources à suggérer à les mettre en commentaire

Behat – jour 4 : API Mink, Sous-contextes et Hooks

29 mar

{lang: 'fr'}

Voici l’avant dernier billet de la série sur la prise en main de Behat. Pour rappel, on a vu :

Allons un peu plus loin :-)

Sortir des sentiers battus : l’API de Mink

Assez rapidement on se retrouve à devoir gérer des cas particuliers, par exemple :

Quand je suis à découvert
Alors le bouton "retirer de l'argent" doit être désactivé

On sort des cas classiques de Mink. Comment faire ?

La solution consiste à développer nous même ce comportement :

/**
 * @Then /^le bouton "([^"]*)" doit être désactivé$/
 */
public function leBoutonDoitEtreDesactive($button)
{
    throw new PendingException();
}

On commence par récupérer notre page à partir de l’objet de session:

$page = $this->getSession()->getPage();

Ensuite on va récupérer l’élément html concerné. Il existe différente manière de faire cela. Le plus simple dans notre cas consiste à passer par un des raccourcis de sélection de Mink : findButton(libellé | id | nom)…

$element = $page->findButton($button);

Si l’élément html n’est pas trouvé, on va lever une exception, sinon on va continuer en faisant une assertion simple : l’élément doit avoir l’attribut « disabled ». Ce qui donne au final :

$page = $this->getSession()->getPage();
$element = $page->findButton($button);

if (null === $element) {
    throw new Behat\Mink\Exception\ElementNotFoundException(
        $this->getSession(), 'element', 'css', $button
    );
}

assertEquals(true, $element->hasAttribute('disabled'));

Au passage, remarquez qu’il s’agit d’une assertion classique de PHPUnit, mais en mode fonction. Pour cela on aura bien entendu ajouté au début de notre fichier :

require_once 'PHPUnit/Autoload.php';
require_once 'PHPUnit/Framework/Assert/Functions.php';

Je vous laisse regarder la documentation ou la feuille d’astuce pour Mink pour plus d’informations. Sachez juste qu’on peut faire pas mal de chose, comme exécuter du JavaScript par exemple (avec $session->evaluateScript() ) ;-)

Organiser son code

Jusqu’ici on a systématiquement mis notre code dans le fichier FeatureContext.php. C’est pas l’idéal : on va très vite se retrouver avec un fichier énorme et imbuvable. Il nous suffit de découper notre contexte en sous-contextes. Tout se fait dans le constructeur du contexte principal :

class FeatureContext extends BehatContext
{

    public function __construct(array $parameters) {
        $this->useContext('mink', new MinkContext($parameters));
        $this->useContext('example1', new MyExample1Context($parameters));
    }

On a donc isolé le contexte de Mink pour en faire un sous-contexte, et on a plus ajouté le nôtre (‘example1′).

L’utilisation des contextes est assez simple. Chaque sous-contexte a un nom (ici ‘mink’ et ‘behat’), que l’on peut utiliser pour les récupérer :

getMainContext()
Récupérer le contexte principal
getSubContext(‘nom’)
Récupérer un sous contexte
getSubcontexts()
Récupérer la liste des tous les sous-contextes

Par exemple :

$session = $this->getMainContext()->getSubContext('mink')->getSession();

On va donc créer le fichier MyExample1Context.php :

class MyExample1Context extends BehatContext
{

    /**
     * @Then /^le bouton "([^"]*)" doit être désactivé$/
     */
    public function leBoutonDoitEtreDesactive($button) {
        $session = $this->getMainContext()->getSubContext('mink')->getSession();
        $page = $session->getPage();
        $element = $page->findButton($button);

        if (null === $element) {
            throw new Behat\Mink\Exception\ElementNotFoundException(
                    $this->getSession(), 'element', 'css', $button
            );
        }

        assertEquals(true, $element->hasAttribute('disabled'));
    }

}

Et voilà, nous voici avec un code découpé et des fichiers plus spécialisés.

Les hooks de Behat

Comme pour les tests unitaires, il est possible d’exécuter du code à certaines phases du déroulement du tests. Il suffit d’utiliser des annotations :

/**
 * @BeforeSuite
 */
public static function prepare(SuiteEvent $event)
{
    // (...)
}

les déclencheurs disponibles sont :

  • BeforeSuite
  • AfterSuite
  • BeforeFeature
  • AfterFeature
  • BeforeScenario
  • AfterScenario
  • BeforeStep
  • AfterStep
  • AfterStep

Un dessin valant mieux qu’un long discours, le plus simple est de regarder ici.

Conclusion

On voit qu’on a quand même peu de limites avec Mink et Behat. Bien plus, on peut même l’intégrer à une PIC (Jenkins, Hudson…). C’est d’ailleurs ce qu’on verra dans le prochain billet, qui sera le dernier de la série ^^

=> Juste par curiosité : j’utilise exclusivement Sahi pour mes tests. Beaucoup de monde utilise Selenium ? C’est mieux ? Vous avez des avis ?

Behat – jour 3 : Tester une application web avec Mink

19 mar

{lang: 'fr'}

On a vu précédemment ce qu’était Behat et comment tester une application simple en php avec Behat.

Maintenant allons plus loin et voyons comment tester une application web (à la quelle on accède par un navigateur). Et oui c’est possible ! :-)

Behat et … Mink

On l’a dit : Behat permet de tester un produit. Ceci dit, il est rare qu’un client vous demande une application en ligne de commande ; le plus souvent le produit en question va être constitué de pages web. Qu’à cela ne tienne, Behat est très fortement lié à un autre outil : Mink.

Mink c’est quoi ? Et bien Mink va vous permettre :

  • ou bien de simuler un navigateur pour interagir avec votre application (produit)
  • ou bien de piloter un vrai navigateur (Firefox, Chrome…) pour interagir avec le produit

Voyons comment le lancer en mode « simulation » dans un premier temps.

Application de test

Pour vous éviter de perdre du temps, je vous propose de télécharger une petite page web toute simple qui va correspondre à notre produit de test :

  • une seule page pour consulter notre compte et ajouter/retirer des sous
  • un formulaire de connexion (pas de mot de passe)
  • les informations sont stockées en session

Ce qui donne ça :

Pour télécharger l’application exemple, ça se passe ici : behat-jour3-appli-web.zip (15 Ko).

Notre produit web

N’oublions pas que le client (product owner) reste le maître de son produit. Il nous fournit donc un fichier de fonctionnalités pour décrire son produit.

Remarque : la fonctionnalité est décrite en anglais ; bien que très actif, utiliser behat et mink en français reste aujourd’hui très aventureux à mon goût, ne serait-ce qu’à cause des nombreux apostrophes présents dans notre langue qui posent des difficultés.

Fichier bank.feature :

Feature: Manage a bank account
  In order to manage my account
  As a logged in user
  I need to be able to add or take mon ey on my account

  Background:
    And I am logged in as "jeanfrancois"
    And I have "50" euro
    And I am on "/"

  Scenario: Check my bank account
    Then I should see "You have 50 euro on your account"

  Scenario Outline: Add money
    Given I have "<initialAmount>" euro
    When I select "<operation>" from "Operation"
    And I fill in "Amount" with "<amount>"
    And I press "Go"
    Then I should see "You have <finalAmount> euro on your account"

    Examples:
    | operation   | initialAmount | amount    | finalAmount   |
    | Add money   | 50            | 10        | 60            |
    | Add money   | 50            | 20        | 70            |
    | Add money   | 50            | 5         | 55            |
    | Add money   | 50            | 0         | 50            |
    | Take money  | 50            | 10        | 40            |
    | Take money  | 50            | 20        | 30            |
    | Take money  | 50            | 30        | 20            |

  Scenario: Overdrafts are not allowed
    Given I have "50" euro
    When I select "Take money" from "Operation"
    And I fill in "Amount" with "60"
    And I press "Go"
    Then I should see "You have 50 euro on your account"
    And I should see "Overdrafts are not allowed"

Nous voilà prêts :-) Notez que les expressions utilisées (I fill in « xxx » with « xx », etc) ne sont pas anodines, bien au contraire. On va le voir par la suite.

Installer Mink

Rien de plus simple :

sudo pear install behat/mink-beta

Maintenant on va « dire » à Behat que l’on souhaite utiliser Mink pour nos tests. Ouvrez le fichier bootstrap/FeatureContext.php, et remplacez

class FeatureContext extends BehatContext {

par

require_once 'mink/autoload.php';
class FeatureContext extends Behat\Mink\Behat\Context\MinkContext {

C’est là que la magie s’opère : Mink dispose déjà de nombreuses expressions disponibles pour tester notre produit web ! On va pouvoir le confirmer en faisant un :

behat -dl --lang=en

Résultat de la commande behat -dl après avoir installé Mink

Il ne nous reste plus qu’à préparer un petit fichier de configuration, nommé feature/behat.yml, qui sera automatiquement lu par behat :

default:
  context:
    parameters:
      base_url: http://localhost/mettez/ici/l/adresse/a/tester

Ici nous indiquons simpelment à Mink la racine de base de notre application, pour éviter de la répéter dans chacun de nos tests.

Tester le produit : Mink crée du sens

Il est temps de se lancer et d’exécuter en ligne de commande :

behat

Miracle ^^ : la majorité des expressions utilisées par notre product owner possède déjà un sens. En tant que développeur, il n’y a que deux expressions pour lesquelles je dois donner du sens :

Mink : résultat de la commande 1 : il reste des étapes à définir.

Quand le product owner spécifie ‘I should see « content »‘, Mink va tout seul faire le lien avec l’assertion « le contenu de la page doit comporter le texte « content ».

Mais bien mieux encore : les champs de mes formulaires ne sont pas reconnus par leur nom ou leur identifiant (name ou id). Non non, le product owner n’en n’a rien à faire de la structure interne d’une page web ; ce qu’il veut c’est un produit qui à l’écran correspond à ses attentes. Les éléments de formulaires sont reconnus par leur libellé (label).

le product owner n’en a rien à faire de la structure interne d’une page web ; ce qu’il veut c’est un produit qui, à l’écran, correspond à ses attentes

Bien sûr, Mink est permissif et on peut tout même cibler un lien, bouton, élément de formulaire… par son identifiant ou son nom… peu importe :-)

Vous trouverez la liste des expressions de base de Mink ici.

Donner son sens aux expressions non définies

Il nous reste donc deux expressions qui n’ont actuellement pas de signification : « Given I am logged in as … » et « I have x euro ». A nous, développeurs, de les définir dans features/bootstrap/FeatureContext.php.

Attention, il y a là un énorme piège. Je l’ai vu à chaque fois, le premier réflexe d’un développeur va être de créer un lien entre le code source de l’application et le code disponible dans le fichier FeatureContext.php. Par exemple en créer une instance d’une Zend_Application comme on le ferait pour des tests unitaires d’une application Zend… C’est la dernière chose à faire !

Non, si le product owner veut un produit web, si Mink nous permet de tester un produit web, continuons de tester un produit web uniquement, et laissons le code de côté :-p .

Le product owner nous dit qu’il est un utilisateur connecté (« I am logged in as… »). Connectons l’utilisateur au sein de l’application :

/**
 * @Given /^I am logged in as "([^"]*)"$/
 */
public function iAmLoggedInAs($username)
{
    return array(
        new Step\Given('I go to "login.php"')
        ,new Step\When("I fill in \"My name\" with \"$username\"")
        ,new Step\When('I press "Login"')
    );
}

Pensez à ajouter au début du fichier :

use Behat\Behat\Context\Step;

Comme vous le voyez, on réutilise les étapes utilisables directement dans nos scénarios. Dans 99% des cas cela suffit. Ce sont les mêmes étapes que le product owner pourrait utiliser dans un fichier de fonctionnalité classique.

Ce qui reviendrait à écrire :

Given I go to "login.php"
When I fill in "My name" with "jeanfrancois"
When I press "Login"

On fait la même chose pour donner du sens à ‘I have « 50″ euro’ :

/**
 * @Given /^I have "([^"]*)" euro$/
 */
public function iHaveEuro($balance) {
    return array(
        new Step\Given('I go to "/"')
        , new Step\When("I fill in \"New balance\" with \"$balance\"")
        , new Step\When('I press "Reset"')
    );
}

Ca y est, on est bons :
Mink : le produit est conforme

Le système de test est fiable car il n’est pas assujetti à des modifications de notre code, mais seulement aux modifications de notre produit. Si l’application venait à ne plus correspondre aux souhaits du product owner, celui-ci en serait immédiatement averti.

Un vrai navigateur, avec du vrai javascript

C’est beau, mais pas très réaliste : quid des applications riches ? Que faire si on a du Javascript et de l’ajax de tous les côtés ?

Et bien on va continuer d’utiliser Mink, mais cette fois-ci on va piloter un vrai navigateur. Mink permet de piloter n’importe quel navigateur en utilisant le driver de notre choix : Sahi ou Selenium (1 ou 2).

Ayant eu de nombreuses déconvenues avec Selenium par le passé, et n’ayant pas encore eu de souci majeur à ce jour avec Sahi, j’ai une nette préférence pour Sahi. Installons le ensemble :

L’instalaltion est simple. Téléchargez Sahi depuis http://sourceforge.net/projects/sahi/files/ et exécutez le :

wget http://sourceforge.net/projects/sahi/files/latest/download?source=files
java -jar sahi_v35_20110719.jar

Cliquez sur suivant, suivant… c’est pas moi qui vais vous apprendre un installer quelque chose :-D .

Lancez ensuite Sahi (placez vous bien dans le dossier spécifié pour éviter les ennuis) :

cd dossier/installation/bin/
./sahi.sh &

Lier des fonctionnalités à un vrai navigateur

Pour utiliser un vrai navigateur à la place de l’émulateur, il suffit d’utiliser le tag @javascript devant le scénario qui le nécessite:

  @javascript
  Scenario: ...

ou devant la fonctionnalité toute entière :

@javascript
Feature: ...

Ensuite on lance behat comme d’habitude :

behat

Vous devriez à ce stade voir se lancer votre navigateur et voir les pages changer toutes seules : les champs se remplissent, ça clique… Bref, ça marche !

Alors bien sûr vous allez me dire : c’est lent ! Et alors ? Ca reste toujours infiniment plus rapide que de tester tout ça à la main, et surtout c’est fiable : si une ligne est rouge, le produit n’est pas livrable ; mais si tout est vert, alors c’est que vous êtes dans les clous et que vous avez bien fait votre boulot. De plus le gain de temps de test manuel et d’aller et retours avec le product owner est considérable. Là c’est vraiment le client qui est le maître de son produit.

Configurer le navigateur

Il nous reste deux ou trois petites choses à voir. En effet, figurez vous que c’est firefox qui est lancé pour les tests, mais que moi je préfererai que ce soit chrome. Pas de problème, modifions le fichier behat.yml :

default:
  context:
    parameters:
      base_url: base_url: http://localhost/mettez/ici/l/adresse/a/tester
      browser: chrome

Il y a pas mal d’options, je vous laisse consulter la petite cheat sheet si besoin ;-)

La prochaine fois on verra comment allez plus loin dans le contrôle du navigateur, en exploitant l’API de Mink, et comment s’organiser pour travailler à plusieurs en créant des sous-contextes personnalisés plutôt que de ranger tout notre code dans un seul fichier.

Mais en attendant, n’hésitez pas à tester Behat et Mink et surtout à dire ce qu’il en est pour vous :-)

Behat – jour 2 : Installation et premiers tests

11 mar

Behat : le produit est conforme
{lang: 'fr'}

Maintenant qu’on a vu à quoi sert Behat, il est temps de passer à la pratique…

Vous trouverez une archive contenant toutes les sources de ce billet en bas de cette page.

Installer Behat

Le plus simple à mon goût : passer par pear :

pear channel-discover pear.symfony.com
pear channel-discover pear.behat.org
pear install behat/behat

Un petit test pour vérifier que tout s’est bien passé :

behat --version

Démarrer un projet

Voici la structure initiale de mon exemple :

mkdir -p  application/library tests/product

Arborescence du projet Behat

Placez-vous dans le dossier tests/product, puis tapez :

behat --init

Cela a pour effet de créer les dossiers nécessaires à votre projet :

  • features : les fichiers de fonctionnalité *.feature
  • features/bootstrap : les classes nécessaires au fonctionnement des fonctionnalités (contextes)
  • features/bootstrap/FeatureContext.php : le contexte principal

Première fonctionnalité

Il est temps de démarrer. Notre client (product owner) souhaite une application dans laquelle on puisse gérer un compte bancaire : on peut consulter son solde, ajouter des sous, en retirer… et on n’a pas le droit d’être découvert.

Le fichier de fonctionnalité livré par notre client ressemble à ceci >feature/banque.feature< :

# language: fr
Fonctionnalité: posséder un compte bancaire
  Afin de gérer les comptes bancaires des utilisateurs
  En tant que client
  Je dois être capable d'effectuer des opérations basique sur mon compte

  Scénario: Avoir un compte bancaire valide
    Etant donné que je suis un nouveau client
    Alors je dois avoir "0" euros sur mon compte

  Scénario: Retirer de l'argent sur mon compte
    Etant donné que je suis un client
    Et que je possède "50" euros sur mon compte
    Quand je retire "10" euros
    Alors je dois avoir "40" euros sur mon compte

  Plan du Scénario: Ajouter de l'argent sur mon compte
    Etant donné que je suis un client
    Et que je possède "<soldeInitial>" euros sur mon compte
    Quand je dépose "<montant>" euros
    Alors je dois avoir "<soldeFinal>" euros sur mon compte

    Exemples:
      | soldeInitial    | montant | soldeFinal |
      | 0               | 10      | 10         |
      | 15              | 5       | 20         |
      | 35              | 5       | 40         |

  Scénario: Interdire les découverts
    Etant donné que je suis un client
    Quand j'essaye de retirer plus d argent que je n en ai sur mon compte
    Alors j'ai un message d erreur "Vous ne pouvez pas être à découvert"

Le product owner sait ce qu’il veut, il nous a même donné des exemples pour mieux nous orienter dans notre développement.

Notez au passage le commentaire « # language: fr » au début du fichier. Il indique que notre fonctionnalité est décrite en français.

Lancer les tests

Il va être temps de lancer behat. Placez-vous dans le dossier tests/product/feature, puis tapez simplement :

behat --lang=fr

(N’oubliez pas de préciser la langue, vu qu’on travaille pour l’instant en français). On va dérouler ensemble le résultat de cette commande :

Rappel de la fonctionnalité

On commence par un rappel de la fonctionnalité et des scénarios testés.

Fonctionnalité Behat

S’il y avait eu des erreurs, le texte aurait été écrit en rouge, et si tout avait été ok, il serait vert. Là le texte est orange : il n’a pas encore de signification par rapport à notre produit.

Bilan du test

On a ensuite des informations sur les tests :

Behat - résultats : en attente

Donner du sens aux fonctionnalités

Toutes nos étapes sont en attente de définition. En effet, on n’a rien qui fait le lien entre les scénarios (les souhaits du product owner) et notre produit (le code source). C’est là que Behat va être à mon sens magique : il nous fournit le code PHP nécessaire pour créer ce lien, avec pour chaque phrase :

  • l’annotation qui permet de faire le lien (@Given /^que je suis un nouveau client$/)
  • la méthode a insérer pour donner du sens à cette phrase (public function queJeSuisUnNouveauClient())
  • les valeurs de cas du scénario (encadrés par des guillemets), qui sont fournis en paramètres de la méthode

Voici donc la dernière étape du traitement du résultat de la commande :

Behat : donner du sens aux fonctionnalités

Behat nous fourni le code a copier-coller vers la classe qui gère notre contexte principal, à savoir la classe FeatureContext contenue dans /features/bootstrap/FeatureContext.php. Allons -y : copiez le code dans le fichier <FeatureContext.php>.

Vous pouvez constater que chaque méthode lance une exception de type PendingException. Cela signifie qu’il va falloir modifier ces méthodes pour les relier à notre application.

Pour vous simplifier la vie, je vous propose de télécharger directement le code nécessaire au bon fonctionnement de ces tests : Account.php, à placer dans application/library. Sinon libre à vous d’écrire le code qui correspondra à l’application ; en soit, peu importe le code, ce qui nous intéresse c’est le produit ;-)

Nous allons maintenant convertir le texte (langue naturelle) en code (source) :

<!--?php use Behat\Behat\Context\ClosuredContextInterface,     Behat\Behat\Context\TranslatedContextInterface,     Behat\Behat\Context\BehatContext,     Behat\Behat\Exception\PendingException; use Behat\Gherkin\Node\PyStringNode,     Behat\Gherkin\Node\TableNode; require_once 'PHPUnit/Autoload.php'; require_once 'PHPUnit/Framework/Assert/Functions.php'; require_once __DIR__ . '/../../../../application/library/Account.php'; use \MyApp\Account as Account; /**  * Features context.  */ class FeatureContext extends BehatContext {     /**      * Testes account      *      * @var \MyApp\Account      */     private $_account;     /**      * Contains the last exception      *      * @var \Exception      */     private $_lastException;     /**      * @Given /^que je suis un nouveau client$/      */     public function queJeSuisUnNouveauClient() {         $this--->_account = new Account;
    }

    /**
     * @Then /^je dois avoir "([^"]*)" euros sur mon compte$/
     */
    public function jeDoisAvoirEurosSurMonCompte($balance) {
        assertEquals($balance, $this->_account->getBalance());
    }

    /**
     * @Given /^que je suis un client$/
     */
    public function queJeSuisUnClient() {
        if(is_null($this->_account)) {
            $this->_account = new Account;
        }
    }

    /**
     * @Given /^que je possède "([^"]*)" euros sur mon compte$/
     */
    public function queJePossedeEurosSurMonCompte($balance) {
        $this->_account->setBalance($balance);
    }

    /**
     * @Given /^je retire "([^"]*)" euros$/
     */
    public function jeRetireEuros($amount) {
        $this->_account->takeMoney($amount);
    }

    /**
     * @Given /^je dépose "([^"]*)" euros$/
     */
    public function jeDeposeEuros($amount) {
        $this->_account->addMoney($amount);
    }

    /**
     * @Given /^j\'essaye de retirer plus d argent que je n en ai sur mon compte$/
     */
    public function jEssayeDeRetirerPlusDArgentQueJeNEnAiSurMonCompte() {
        try {
            $this->_account->setBalance(50);
        $this->_account->takeMoney(100);
        } catch (\Exception $e) {
            $this->_lastException = $e;
        }
    }

    /**
     * @Given /^j\'ai un message d erreur "([^"]*)"$/
     */
    public function jAiUnMessageDErreur($message) {
        assertEquals($message, $this->_lastException->getMessage());
    }

}

Vous constaterez que :

  • on utilise PHPUnit en mode « fonction » pour nos assertions
  • chaque méthode correspond à une phase d’un scénario
  • on fait ce qu’on veut à l’intérieur de notre contexte ^^
  • c’est simple : il ne faut même pas 3 minutes pour écrire ce code

L’heure de vérité

Le suspense est à son comble : notre code correspond t–il au souhait du client quant à son produit ?

behat --lang=fr

Behat : le produit est conforme

Conclusion

Si besoin voici une archive contenant l’ensemble de ce projet : Découverte de behat – jour 2.zip.

Notre client (product owner) peut désormais s’assurer à tout moment que son produit est valide.Bien plus, il va pouvoir ajuster son produit au fur et à mesure des sprints en vous décrivant clairement son besoin : fini les specs de 500 pages, c’est le product owner qui est maître de son produit ! Et cela grâce à Behat.

La prochaine fois on verra comment mieux organiser son code : sous-contextes, configuration… et surtout comment tester une application web, grâce à Mink. mais en attendant, toute remarque / commentaire est le bienvenu ;-)

Je vous rappelle qu’une cheat sheet pour behat est disponible.

Cheat Sheet Behat

6 mar

Cheat Sheet Behat Mink - page 1
{lang: 'fr'}

Je vous propose une petite feuille d’astuces (cheat sheet) pour Behat et Mink. N’hésitez pas pas à vous en servir et vous en resservir :-)

PDF : Cheat Sheet Behat and Mink

Edit : The English version : PDF (en) : English Behat Cheat Sheet

Image :

Cheat Sheet Behat Mink - page 1
Page 1
Cheat Sheet Behat Mink - page 2
Page 2

Au programme :

Page 1 :

  • Utilisation de la ligne de commande
  • Behat en 2 mots
  • Les fichiers de fonctionnalités et les scénarios
  • Insérer des exemples dans les scénarios
  • Naviguer avec Mink

Page 2 :

  • Infos sur la Session
  • Infos sur les éléments
  • Infos sur les noeuds HTML
  • Configuration par défaut (behat.yml)
  • Drivers disponibles pour Mink

Pour découvrir Behat, je vous invite à consulter le billet Jour 1 – Comprendre et utiliser Behat pour tester un produit Scrum ?

Tout commentaire est le bienvenu (ce qui manque, n’est pas clair, etc…). Merci de vos retours !

Behat – jour 1 : comment tester son produit SCRUM ?

2 mar

{lang: 'fr'}

Ce billet démarre une série consacrée à cet outil que je trouve génial : Behat. Désolé pour la longueur, mais le sujet est suffisamment intéressant à mon goût pour être creusé comme il faut. :-)

Pour ce premier jour avec Behat, on va se placer du côté du client uniquement. la partie développeur viendra dans le prochain billet. Nous allons donc découvrir Behat pas à pas.

Behat, c’est quoi ? Pour le comprendre, il faut d’abord faire un tour du côté des méthodes agiles, et particulièrement du côté de SCRUM

Scrum – la notion de produit et de test d’acceptation

J’abstrais volontairement une partie des aspects de Scrum pour me consacrer uniquement aux points déterminants pour comprendre les enjeux de Behat. Vous trouverez plus de détails sur Scrum ici).

Scrum, c’est avant tout un état d’esprit, mais c’est aussi un exemple d’organisation. Imaginez un peintre à qui l’on demande de peindre Mona Lisa. Si on fait l’analogie avec le développement, la manière classique consiste à peindre le tableau de haut en bas, comme le ferait une imprimante. Le tableau ne pourra être livré qu’une fois la peinture entièrement achevée, de bas en haut.

(les images proviennent de Jeff Patton)

Le développement incrémental - mona lisa

Au contraire, en Scrum, on va commencer par esquisser les traits de Mona Lisa, puis on va demander au client si ça lui convient. Puis on va affiner le trait et le dessin. On redemande au client si ça lui convient. Enfin on va pouvoir ajouter des touches de couleurs, etc. C’est ce qu’on appelle un développement itératif. A chaque itération (« release« ), le client valide le produit fourni et décide ou non de continuer. S’il s’arrête, le produit est simplement moins riche qu’escompté, mais il est livrable et utilisable par le client.

Le développement itératif - mona lisa

En Scrum, le client est donc le maître du produit qu’on lui fourni : il a la main dessus et doit participer activement a sa description fonctionnelle. On dit alors qu’il est le propriétaire du produit (Product Owner).

Pour décrire, justement, son produit, le client fourni généralement des spécifications fonctionnelles détaillées (SFD – qui, au passage, ne sont en général jamais lues car trop longues). Cette fois le client va au contraire décrire des cas d’utilisation de son produit. Pour savoir si le produit livré correspond à son besoin, il lui suffira de s’assurer que les cas d’utilisation sont respectés. Ces cas d’utilisation, garants du produit, sont regroupés dans tests d’acceptations du produit.

A chaque itération (release), le produit livré est donc comparé au produit souhaité au moyen de ces tests d’acceptation. C’est la garantie :

  • de satisfaire la demande initiale du client
  • d’éviter les quiproquos (« mais moi je voulais pas ça »)
  • d’éviter les spécifications trop volumineuses
  • d’éviter l’ajout de nouvelles fonctionnalités par le client au fur et à mesure du développement (mais il pourra ajouter des fonctionnalités lors d’une prochaine release)

Behat dans tout ça ?

Behat, c’est l’outil PHP qui va permettre de faire le lien entre ces tests d’acceptation, écrits par le Product Owner, et le produit que moi, développeur, je lui livre. Bien plus, il permet de lancer ces tests d’acceptation de manière automatisée sur le produit et d’en fournir un rapport lisible par n’importe quel client. A chaque release, en un coup d’oeil, le client voit si ce qu’on lui a livré correspond à son besoin.

Attention ! Behat ne se substitue pas aux tests unitaires. Non, au contraire. Ce qu’il faut se dire c’est que le client n’en a rien à faire de savoir  que le code est de bonne qualité ; ce qu’il veut c’est un produit fini, fonctionnel, conforme à ses souhaits initiaux. Le test unitaire, lui, va nous permettre de nous assurer de l’intégrité du code pour des fonctionnalités sensibles ou complexes. Il ne teste pas le comportement du produit (mais le test unitaire reste cependant indispensable)

Le développeur a en effet souvent tendance à détourner ses tests unitaires de leur fonction initiale (tester du code), pour tester des scénarios (par exemple qu’un Controlleur a le comportement attendu). Ce n’est pas aux outils de tests unitaires de faire cela. Heureusement, Behat est là pour ça ;-) !

Comment ça marche ?

Bon, c’est bien, mais plus concrètement ça marche comment ?

Et bien le Product Owner va rédiger ses tests d’acceptation en langue naturelle. Chaque fonctionnalité va être écrite dans un <fichier nom_de_la_fonctionnalite.feature>.

Il commencer par une description de sa fonctionnalité :

Fonctionnalité: Avoir un compte bancaire
  Afin d'offrir aux utilisateurs la possibilité d'avoir un compte bancaire
  Etant donné que je suis inscrit
  Je dois être capable d'ajouter ou de retirer de l'argent sur mon compte

Cette description permet de comprendre la fonctionnalité de manière générale. Ensuite il va fournir des cas d’utilisation qui vont décrire cette fonctionnalité. On parle alors de « Scénario« . Chaque scénario est défini par :

  • un contexte (étant donné que)
  • des événements déclencheurs (quand)
  • un résultat attendu (alors)
Scénario:
  Etant donné que je suis un utilisateur connecté
  Et que j'ai un compte bancaire
  Et que le solde de mon compte est de "10" euros
  Quand j'ajoute "5" euros sur mon compte
  Alors mon solde doit être de "15" euros

Il va pouvoir combiner les scénarios :

Scénario:
  Etant donné que j'ai un compte bancaire
  Et que le solde de mon compte est de "10" euros
  Quand j'ajoute "5" euros sur mon compte
  Alors mon solde doit être de "15" euros

Scénario:
  Etant donné que j'ai un compte bancaire
  Et que le solde de mon compte est de "10" euros
  Quand je retire "50" euros sur mon compte
  Alors je dois avoir le message d'erreur "vous n'avez pas le droit d'être à découvert"
  Et mon solde doit être de "10" euros

Quand je lancerai Behat, si mon application fonctionne correctement, ce scénario va être valide et donc coloré en vert à l’écran.Sinon il sera rouge.

Mieux encore, le Product Owner va pouvoir insérer des exemples. Behat va automatiquement lancer ces tests pour chaque exemple fourni :

Scénario Outline:
  Etant donné que j'ai un compte bancaire
  Et que le solde de mon compte est de "" euros
  Quand j'ajoute "" euros sur mon compte
  Alors mon solde doit être de "" euros

  Examples:
    | soldeInitial | montant | soldeFinal |
    | 5            | 10      | 15         |
    | 20           | 20      | 40         |
    | 20           | 7       | 27         |
    | 0            | 10      | 10         |

Pas mal non ? Je vais avoir un rapport visuel (une page web par exemple) de l’état de mon produit par rapport à ces tests :

Exemple de rapport de test d'acceptation avec Behat

Maintenant vous avez compris le principe du test d’acceptation. Vous vous êtes mis dans la peau d’un Product Owner qui rédige ses tests d’acceptation. Si vous êtes Product Owner, vous n’avez pas besoin d’aller plus loin. Si vous êtes développeur ou Scrum Master, on verra ensemble le versant technique de Behat dans le prochain Billet ;-)

Edit: le jour 2 est prêt : Behat – jour 2 : Installation et premiers tests

Atelier PHP sur Orléans – Tester son code / produit

29 fév

{lang: 'fr'}

Comme chaque premier jeudi du mois, demain se tiendra un atelier afup à Orléans.

Au programme :

  • le développement piloté par les tests (TDD)
  • tester son code avec phpUnit
  • tester son code avec Atoum
  • tester son produit avec Behat

Les infos nécessaires sont ici

Je n’ai pas prévu de slides : uniquement du code ;-) . Nous échangerons ensuite sur le thème du test en général et sur les outils présentés…

Tout le monde est le bienvenu : membres et non-membres de l’AFUP, développeur confirmé ou non.

A demain !

Utilisez un moteur javascript en PHP et faites exploser vos perfs

25 fév

{lang: 'fr'}

Bon, je l’admets, ce titre est un brin accrocheur et trolleur :-) . Ceci dit, je viens de tomber sur Twitter sur un package PECL que je ne connaissais pas et qui à mon avis ouvre des perspectives très intéressantes, notamment pour améliorer les performances de certaines fonctionnalités chronophage de PHP.

Sur certains de mes tests (voir plus bas), le résultat est juste… 300 fois plus performant !

Ce package, V8JS, permet d’utiliser dans PHP le célèbre V8 JavaScript Engine. Ce moteur, développé par Google et très performant, est le moteur javaScript de Chrome.

Installation du V8 JavaScript Engine

On va commencer par installer le moteur en lui-même :

sudo apt-get install libv8-dev
sudo apt-get install libv8-3.1.8.22

Installation du package pear

sudo pecl install v8js-beta php_ini=/chemin/vers/le/php.ini

Puis modifier le php.ini pour inclure le module v8js.so

[v8js]
extension=/chemin/du/module/v8js.so

Vous pouvez déplacer le module v8js.so dans le dossier des modules de PHP, ce qui permet d’avoir plus simplement :

[v8js]
extension=v8js.so

Un petit phpinfo() pour vérifier :

phpinfo du moteur Javascript V8 en PHP

Utiliser V8JS

les choses sérieuses commencent ! Examinons la documentation. Plutôt simple : on a une classe V8Js, et une méthode executeString() pour appeler le moteur :

$v8 = new V8Js();
$JS = <<<EOT
function car(){
}
var myCar = new car();
myCar.color = 'green';
print(myCar.color);
EOT;

try {
  $v8->executeString($JS);
} catch (V8JsException $e) {
  var_dump($e);
}

On a donc utilisé le moteur javascript pour :

  • déclarer une classe
  • instancer un objet
  • ajouter un attribut à cette instance

Les performances

C’est là qu’on tombe dans des choses intéressantes.

Parenthèse :
Attention, comme tout bench, ces mesures sont à remettre dans leur cadre. ici je teste un type de comportement, dans un contexte donné, sur ma machine. De plus c’est orienté, j’ai délibérément choisi quelque chose pour lequel JavaScript excelle.

Bref, en voyant ça, je me suis dit : tiens, et si je testais un peu les performances ? J’ai alors cherché un comportement pour lequel JavaScript est particulièrement performant, et qui existe également en PHP. J’ai tout de suite pensé aux fonctions array_reduce / reduce. Voici deux codes qui me semblent comparables.

Voici le premier, en PHP pur :

<?php
// bench
$start = microtime(true);

//
// Test
$myArray = array();
for ($i = 0; $i < 100000; $i++) {
    array_push($myArray, $i);
}

function rsum($v, $w) {
    $v += $w;
    return $v;
}

$r = array_reduce($myArray, "rsum");

//
// Bench
$duration = microtime(true) - $start;
var_dump($duration);

Dans l’autre, on reporte les calculs sur le moteur JavaScript :

//
// bench
$start = microtime(true);

$js = <<<EOT
var i;
var myArray =  [];
for (i = 0; i < 100000; i++) {
    myArray.push(i);
}

function rsum(v, w) {
    v += w;
    return v;
}

var r = myArray.reduce(rsum);
print(r);
EOT;

//
// Test
$v8 = new V8Js();
try {
  $v8->executeString($js);
} catch (V8JsException $e) {
  var_dump($e);
}

les résultats

Voici ce que j’obtiens :

  • en pur PHP : 2.5989
  • en reportant sur le moteur javascript : 0.0085

Bref, comme je le disais plus haut : un sacré écart !

Conclusion sur le moteur V8 en PHP

je n’ai aucun recul sur cette utilisation du moteur V8 au sein de PHP. Le package est d’ailleurs en bétâ. Cependant, je pense que c’est une piste sérieuse si on cherche à reporter certains traitements (particulièrement des calculs) vers JavaScript…

Le point sur les limites du typage de PHP

20 fév

{lang: 'fr'}

Les limites

PHP a ceci de particulier qu’il est est un langage de typage faible (le type des variables peut changer en cours de route), mais qu’il autorise un typage fort partiel des paramètres de fonctions pour ce qui concerne les objets et les tableaux. On pourra ainsi écrire :

function test(array $argument) {
    (...)
}

 

function test(monObjet $argument) {
    (...)
}

mais par contre il n’est pas possible de typer les paramètres de méthodes pour les types scalaires (entier, chaînes de caractères…)

function test(string $argument) {
    // qui a dit que PHP ne savait pas être drôle ?
    // Argument 1 passed to test() must be an instance of string, string given
}

Pourtant, ça semble bien souvent manquer : combien de fois vois t-on des tests de type dans un code ?

function test($argument) {
    if(!is_string($argument)) {
        throw new InvalidArgumentException(&quot;eh ! on voulait une chaîne !&quot;);
    }
}

Bref, on se retrouve avec du code pas vraiment utile (qui n’est ni métier ni applicatif), qui, bien souvent, nuit à la lecture et à la bonne compréhension des sources.

Les solutions pistes

il existe cependant des solutions, ou au moins des tentatives de solutions, pour pallier à ces inconvénients.

Patch

La première provient d’un modification du langage lui-même, sous forme d’un patch ou d’une extension. Ilia Alshanetsky propose depuis longtemps un patch pour PHP 5.3. Ce patch, complet, modifie le comportement du parseur, mais complète également Reflection.
Malheureusement, l’utilisation de ce patch nécessite de devoir patcher systématiquement PHP, avec d’autant plus de risques que le patch n’est pas officiel. Mais surtout ce patch n’est pas mis à jour, et n’est plus compatible avec les versions récentes de PHP 5.3 (ne parlons même pas de PHP 5.4 :-) )

La Standard Php Libary

Bon, c’est un idée, mais il y en a eu d’autres. Marcus Börger et David Coallier ont développé l’extension PECL SPL_Types pour insérer ce comportement dans la SPL.

Ici, pas vraiment de typage fort scalaire, mais une surcouche à utiliser :

function test(SplInt $integer){
    // (...)
}

$value = new SplInt(5);
test($value);

Ça semble marcher, mais personnellement je trouve ca peu élégant et pas vraiment optimal : quid de la compatibilité avec des librairies externes qui n’utiliseront pas les typages Spl ? Et bon, on ne type pas les scalaires ; dans notre exemple ce qui suit ne fonctionnera pas, ce qui montre en soi que cette solution n’est qu’une rustine :

assert(5 instanceof SplInt); // faux
assert(is_int( new SplInt(5) )); // faux

La rustine de la mort qui tue

Très vite, de nombreux développeurs ont eu l’idée de passer par un gestionnaire d’erreur personnalisé pour résoudre ce problème. Personnellement, je l’avais mis en place sur un framework dans une ancienne boite. À l’époque j’avais trouvé ça tout seul et ça m’avait bien amusé. Après j’avais fait des benchs et je l’avais vite retiré ;-)

L’idée est d’intercepter les erreurs, d’analyser le message de l’erreur et de voir s’il correspond pas à un problème de type. Si c’est le cas on fait nous même le contrôle « a la main », sinon on renvoie vers le gestionnaire d’erreur par défaut.

Voici une version basique :

set_error_handler(function($errno, $errstr, $errfile, $errline) {
    if(preg_match('!must be an instance of (\w*), (\w*) given!', $errstr, $matches)) {
        $matches[2] = str_replace('double','float', $matches[2]);
        return strtolower($matches[1]) == strtolower($matches[2]);
    }
});

En 6 petites lignes de code, magique : on peut désormais écrire ceci :

function example1(integer $v) {
    echo 'Cool, integer was given !'.PHP_EOL;
}
function example2(string $v) {
    echo 'Cool, string was given !'. PHP_EOL;
}
function example3(float $v) {
    echo 'Cool, float was given !';
}
function example4(boolean $v) {
    echo 'Cool, boolean was given !';
}
function example5(object $v) {
    echo 'Cool, object was given !';
}
function example6(resource $v) {
    echo 'Cool, resource was given !';
}

N’oubliez pas que c’est catastrophique côté perf :-)

Conclusion

Bref, on se retrouve avec des rustines et pas vraiment des solutions exploitables en production. Pourtant, si je comprend que le typage fort puisse gêner, je trouve que le typage des paramètres et des retours de de fonction (comme en Java par exemple) manque beaucoup, et pourrait apporter du grain dans la professionnalisation du langage.

D’ailleurs, si on observe bien, on retrouve tout de même de nombreuses RFC à ce sujet, et le sujet à été débattu à de nombreuses reprises, et par de nombreuses « figures » de PHP (Derick Rethans, Sebastian Bergmann…), on a même un moment que c’était bon. Aux dernières nouvelles (mais je dois avouer que je ne sais pas trop ce qu’il en est ) il me semble que l’introduction du typage fort des arguments des fonctions reste très largement discutée, voire contestée, et qu’alors même on l’espérait pour php 5.4, elle n’est prévue dans aucune des futures versions sur les rails.

Quel est votre avis sur la question ? Le typage fort des paramètres et des retours de fonctions vous manque t-il au quotidien ? Ou bien au contraire trouvez-vous que c’est justement la force de PHP que d’offrir une grande souplesse aux développeurs et de s’en passer ? Etes-vous déçus de ne pas avoir cette nouveauté dans PHP 5.4 ?