00:00:00

Sur la route de la qualité Introduction aux tests

P. Tachoire et J. Muetton

ITNetwork, 6 décembre 2012

Notes

Sommaire

Introduction aux tests

  • Pourquoi les tests ?
  • Quels tests ?
  • phpUnit
  • Un exemple dirigé

Notes

Pourquoi des tests ?

Notes

Préjugés

Sondage: parmi vous, qui écrit des tests ?

Pourquoi ne pas écrire de tests ?

  • Ecrire des tests c'est rébarbatif
  • Les tests c'est long à écrire
  • Ce n'est pas nécessaire, mon code fonctionne !
  • Le code complexe ne peut être testé

Notes

Ecrire des tests c'est lourd

Souvent, les débutants écrivent les tests

  • après avoir développé le projet
  • de manière concentrée en une seule fois

Ce genre d'expérience est aussi traumatisante qu'écrire son css à la fin du projet !

Quelques solutions :

  1. Ecrire des tests au fur et à mesure du développement permet de leur donner un interêt
  2. Le TDD préconise même de les écrire en premier, pour finaliser les spécifications !

Notes

Tester prend du temps

Ecrire un test prend effectivement du temps, mais :

  • Vous écrivez déjà des tests (echo, var_dump)
  • Plus d'oublis de scenario
  • Son exécution est rapide
  • Détection immédiate des effets de bord
  • Son écriture facilite les spécifications

VRAI

  • Mais le gain de temps est supérieur
  • Maintenance facilitée, même 6 mois plus tard !
  • Si vous ne testez pas, qui rapporte les effets de bord ?

ARGUMENT INVALIDE !

Notes

Les tests ne font pas partie du produit fini alors pourquoi prendre du temps à les écrire ?

Question légitime, mais :

  • echo et var_dump sont une façon de tester que vous retirez une fois le scenario validé.
  • Le scénario est toujours le même, et s'exécute plus rapidemement que le combo +clic.
  • En cas de bug, toujours commencer par écrire un test, c'est l'assurance de non régression ainsi que le gage de corriger le problème.
  • Une bonne stratégie de tests permet de faire du refactoring tout en garantissant la qualité.

Au final c'est beaucoup de temps gagné sur la vie du projet, on corrige tellement souvent les bugs au cours du développement, et en si peu de temps que le risque est de ne pas se rendre compte à quel point c'est rentable.

Exemple d'effet de bord: ajouter un format/la timezone à un parser de date

Pourquoi tester, ca marche ?

Les tests c'est une sorte de filet de sécurité.

  • Assurer une non régression
  • Permettre une refactorisation du code
  • Simplifier la maintenance
  • Une manière simple de rentrer dans le code
  • Faciliter la vie du nouvel arrivant

Tester c'est anticiper les défauts.

Le contrat de maintenance reste le même, mais le temps passé est réduit.

Attention :

De mauvais tests peuvent coûter plus cher que pas de tests du tout !
On reste simple et on ne pense à la fonctionnalité, pas au code.

Notes

Un code qui marche c'est un code à l'épreuve de la production depuis plusieurs années, pour les autres, gardez un oeil sur le bugtracker...

Les erreurs d'intégration sont repérées immédiatement, donc les nouveaux développements sont plus rapides.

Les tests c'est une sorte de documentation pour les développeurs. Toutes les méthodes présentent des cas d'utilisation avec le résultat attendu.

Le code complexe se teste mal

Il ne faut pas confondre Code complexe et code complexe construit sur des outils plus simples

  • Découpler pour rester testable
  • Réfléchir a la testabilité pour simplifier l'API
  • Tester pour faciliter le debuggage
  • L'idéal est d'envisager les tests dès la conception

FAUX : Le code complexe est prioritaire !

Mais rappelez vous bien :

c'est Chuck qui maintient votre code.

Il connait votre adresse !

Notes

Il est important de garder en tête de rester testable, c'est même le principal objectif quand on code.

Si votre code n'est pas testable, alors:

  • comment un collègue peut le comprendre (debug, release master, ...) ?
  • comment vous allez le reprendre (maintenance, nouveau développement, ..) ?

Ecrire un test commence toujours par un cas d'utilisation, il faut le prendre comme une spécification. Un code complexe doit être spécifié, pourquoi pas via un test ?

Exemple

Un exemple de test qui a permis de définir une api simple et compréhensible

function testAfterParkingDestinationIsTheNewLocation()
{
    $boumbo = new Car($location = 'Clermont ferrand');
    $boumbo->startEngine()
           ->driveTo($destination = 'Paris')
           ->park();

    $this->assertEquals('Paris', $boumbo->getLocation());
}

Il n'est jamais trop tard, alors même si votre projet est commencé, écrivez vos premiers tests dès à présent.

Notes

Quels tests ?

Notes

Quels tests ?

Dans notre arsenal il y a :

  • des tests unitaires
  • des tests fonctionnels
  • des tests d'intégration
  • des tests de charge / performance
  • ...

Notes

Tests unitaires

Tester un module ou une fonctionnalité indépendamment du reste du programme.

  • Quand une voiture a démarré, le moteur tourne.
  • Quand un formulaire reçoit des données valides, ce formulaire est valide.
  • Un administrateur peut ajouter un salarié.

Nous testons une seule fonctionalité dans un cas de figure prédéterminé.

Notes

Tests fonctionnels

Test du logiciel d'un point de vue utilisateur du composant ou du logiciel.

  • Cliquer sur "Ajouter un employé"
  • Un formulaire apparaît
  • Soumettre le formulaire avec les données de "Jean"
  • Je suis redirigé sur l'accueil
  • Avec une voiture neuve
  • Située à "Clermont Ferrand"
  • Quand je conduis jusqu'à "Paris"
  • Le compteur indique "456km"

Les services externes peuvent être simulés pour aller plus vite et assurer la validité du scénario.

Notes

Tests d'intégration

Test avec les différents éléments conformément à l'environnement de production.

  • L'application peut traiter 1000 utilisateurs simultanés
  • Les écritures sont faites sur le maître et l'esclave
  • L'API de paiement répond positivement à notre requête

Nous sortons du rôle du développeur et entrons en territoire devops, voir admin sys.

Notes

Méthodologies

Pour chaque étape, selon les goûts et les besoins :

  • Test Driven Development (TDD)
    • écrire ses tests avant
    • si je passe le test, je m'arrête
  • Behaviour Driven Development (BDD)
    • décrire le comportement
    • un état initial
    • une action
    • une attente

Notes

Note sur l'intégration continue

  • Lancer les tests à chaque reversement du code
  • vérifier les "coding standards"
  • prévenir le développeur en cas de problème
  • packager la version ?

une visibilité permanente sur la qualité

Notes

c'est automatiser les outils de qualité autour du code

Outils

  • Tests navigateur headless
    • Behat + Mink
    • casperjs / phantomjs
    • zombie + mocha
    • selenium2
  • PHP pur
    • phpUnit
    • simpleUnit
    • Atoum
    • Behat
  • environnements d'intégration continue
    • Jenkins
    • Travis-ci
    • Gitlab-ci
  • ...

Notes

phpUnit

Notes

phpUnit

  1. intaller phpUnit
  2. les assertions
  3. mon premier test
  4. les data provider
  5. tester les sorties
  6. tester une exception

Notes

Installer via phar

La méthode facile.

$ wget http://pear.phpunit.de/get/phpunit.phar
$ chmod +x phpunit.phar

Si suhosin est installé vous devez autoriser les phar:

# /etc/php5/cli/conf.d/suhosin-allow-phar.ini
suhosin.executor.include.whitelist="phar"

Notes

Installer via composer

Créer un composer.json avec le contenu

{
    "name": "phpunit",
    "description": "PHPUnit",
    "require": {
        "phpunit/phpunit": "3.7.*"
    },
    "config": {
        "bin-dir": "/usr/local/bin/"
    }
}

puis

sudo ./composer.phar install

Notes

Lancer les tests

# lancer un scenario
$ phpunit Test/MyFirstTest.php

# lancer un ensemble de scénarii
$ phpunit Test/Model

# lancer la stratégie complète
$ phpunit -c phpunit.xml Test

Ce qui nous donne :

....................................................  63 / 147 ( 42%)
.................................................... 126 / 147 ( 85%)
...........

Time: 21 seconds, Memory: 43.50Mb

OK (147 tests, 367 assertions)

Notes

Les assertions

Une assertion est une expression qui doit être évaluée à vrai.

  • assertTrue($result);
  • assertNull($result);
  • assertEquals($expected, $result);
  • assertFileExists($pathToFile);
  • assertCount($count, $result);

Toutes les assertions sur la documentation de phpunit

Notes

Les tests par la pratique

Nous voulons afficher les n premiers mots d'un texte

Par exemple:

\Text\excerpt("Je mange des carottes et des petits pois", 5);
// nous donne "Je mange des carottes et..."

ou encore :

\Text\excerpt("ITNetwork rules", 5);
// nous donne "ITNetwork rules"

Notes

Le fichier de test

Dans un fichier Test/ExcerptTest

namespace \Test\ExcerptTest;
include __DIR__ . "/../src/text.php";

class ExcerptTest extends \PHPUnit_Framework_TestCase
{
    public function testTruncate()
    {
        $length = 5;
        $given  = 'Je mange des carottes et des petits pois';
        $expect = 'Je mange des carottes et...';

        $result = \Text\excerpt($given, $length);

        $this->assertEquals($expect, $result);
    }
}

Notes

  • Le fichier doit finir par Test.php
  • Exécute toutes les méthodes commençant par test
  • Toujours passer le résultat attendu puis la valeur à tester

La fonction

Ecrivons une fonction simple pour commencer

// src/text.php
namespace Text;

function excerpt($text, $length = 10)
{
    return $text;
}

Notes

Lançons les tests

$ phpunit Test/ExcerptTest.php

Qui nous donne

F

Time: 0 seconds, Memory: 2.75Mb

There was 1 failure:

1) Test\ExcerptTest\ExcerptTest::testTruncate
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'Je mange des carottes et...'
+'Je mange des carottes et des petits pois'

/home/julien/Projects/test/Test/ExcerptTest.php:15

FAILURES! Tests: 1, Assertions: 1, Failures: 1.

Notes

Faire passer les tests

Modifions comme suit :

function excerpt($text, $length = 10)
{
    $words = explode(' ', $text);
    $excerpt = array_slice($words, 0, $length);

    $res = join(' ', $excerpt);
    return $res . '...';
}

puis lançons les tests

PHPUnit 3.6.10 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 2.75Mb

OK (1 test, 1 assertion)

Notes

Ajouter des tests

Créons une nouvelle assertion dans notre classe de tests

public function assertTruncateFiveOk($expect, $given)
{
    $result = \Text\excerpt($given, 5);

    $this->assertEquals($expect, $result);
}

Notes

Ajouter des tests

Testons notre second cas

public function testTruncate()
{
    $given  = 'Je mange des carottes et des petits pois';
    $expect = 'Je mange des carottes et...';
    $this->assertTruncateFiveOk($expect, $given);
}

public function testTruncate2()
{
    $given  = 'ITNetwork rules';
    $expect = 'ITNetwork rules';
    $this->assertTruncateFiveOk($expect, $given);
}

Autre méthode : Les dataprovider à la rescousse

Notes

Data provider

/** @dataProvider truncateProvider */
public function testTruncate($given, $expected)
{
    $result = \Text\excerpt($given, 5);
    $this->assertEquals($expected, $result);
}

et le provider

public function truncateProvider()
{
    return array(
        'simple test'  => array(
            'Je mange des carottes et des petits pois',
            'Je mange des carottes et...'),
        'shorter test' => array(
            'ITNetwork rules', 'ITNetwork rules'),
    );
}

Notes

Plus de tests

class ExcerptTest extends \PHPUnit_Framework_TestCase
{
    public function truncateProvider()
    {
        return array(
            'simple test'  => array(
                'Je mange des carottes et des petits pois',
                'Je mange des carottes et...'),
            'shorter test' => array(
                'ITNetwork rules', 'ITNetwork rules'),
            'exact length' => array(
                'un deux trois quatre cinq',
                'un deux trois quatre cinq'),
            'hyphen'       => array(
                'y a-t\'il un pilote dans l\'avion ?',
                'y a-t\'il un pilote...'),
        );
    }
    /* ... */
}

Notes

Résultats

$ phpunit Test/ExcerptTest.php

Retourne :

.FFF

-'ITNetwork rules'
+'ITNetwork rules...'

-'un deux trois quatre cinq'
+'un deux trois quatre cinq...'

-'y a-t'il un pilote...'
+'y a-t'il un pilote dans...'

FAILURES! Tests: 4, Assertions: 4, Failures: 3.

à vous de le faire passer :)

Notes

6 mois plus tard...

On veut aussi pouvoir tronquer des chaines avec du html

  • On se concentre sur notre nouveau test
  • Une non-régression est assurée par les tests existant

Ce qui donne :

public function truncateProvider()
{
    return array(
        /* ... */
        'html'  => array(
            '<pre>Hello Moto &amp; Altra&acute;</pre>',
            'Hello Moto &amp; Altra&acute;'),
    );
}

Notes

Tester les sorties

function output_something($something) {
    print $something;
}

Peut se tester via

class OutputTest extends \PHPUnit_Framework_TestCase
{
    public function testOutput(
            $input = "foo bar baz", $result = "foo bar baz")
    {
        $this->expectOutputString($result);
        output_something($input);
    }
}

Notes

Tester les exceptions

function throw_an_exception() {
    throw new \LogicException('Please implement me');
}

Peut se tester via

class OutputTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @expectedException           LogicException
     * @expectedExceptionMessage    Please implement me
     */
    public function testThrowException()
    {
        throw_an_exception();
    }
}

Notes

setUp et tearDown

Exécuter du code avant et après chaque methode de test.

class StackTest extends PHPUnit_Framework_TestCase
{
    protected $stack;

    protected function setUp()
    {
        $this->stack = array();
    }

    public function testEmpty()
    {
        $this->assertTrue(empty($this->stack));
    }
}

Pour initialiser les dépendances, définir l'environnement...

Notes

setUpBeforeClass et tearDownAfterClass

Exécuter du code avant et après une classe de test.

class DatabaseTest extends PHPUnit_Framework_TestCase
{
    protected static $dbh;

    public static function setUpBeforeClass()
    {
        self::$dbh = new PDO('sqlite::memory:');
    }

    public static function tearDownAfterClass()
    {
        self::$dbh = NULL;
    }
}

Notes

phpunit.xml

Configuration de la stratégie de test de son projet

Notes

Le plus simple

<?xml version="1.0" encoding="UTF-8"?>
<phpunit>
    <testsuites>
        <testsuite name="Object_Freezer">
            <directory>Tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

Pour lancer les tests:

$ phpunit -c phpunit.xml Test/ExcerptTest.php

Toutes les directives dans la doc de phpunit

Notes

Un vrai projet...

<phpunit
    colors                      = "true"
    convertErrorsToExceptions   = "true"
    convertNoticesToExceptions  = "true"
    convertWarningsToExceptions = "true"
    syntaxCheck                 = "false"
    bootstrap                   = "bootstrap.php.cache" >

    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>../src/*/*Bundle/Tests</directory>
            <directory>../src/*/Bundle/*Bundle/Tests</directory>
        </testsuite>
    </testsuites>

    <filter>
        <whitelist>
            <directory>../src</directory>
            <exclude>
                <directory>../src/*/*Bundle/Resources</directory>
                <directory>../src/*/*Bundle/Tests</directory>
                <directory>../src/*/Bundle/*Bundle/Resources</directory>
                <directory>../src/*/Bundle/*Bundle/Tests</directory>
            </exclude>
        </whitelist>
    </filter>

    <logging>
        <log type="junit" target="../build/logs/junit.xml"
            logIncompleteSkipped="false"/>
    </logging>

</phpunit>

Notes

Calcul du score d'une partie de tennis

Notes

Code

https://github.com/cup-of-giraf/unittesting-sample

Notes

Merci

Notes

Notes