Petite aide !

  • Appuyer sur 'h' pour surligner les snippets
  • Appuyer sur 'p' pour afficher les notes (s'il y en a sur la slide en court)
  • Appuyer sur 'f' pour mettre en plein écran
  • Appuyer sur 'w' pour basculer en écran large
  • Appuyer sur 'o' pour bascule en mode présentation
  • Appuyer sur 'ESC' pour quitter les modes ci-dessus

Qu'est ce qu'un test ?

  • Par définition (Wikipédia) un test est un procédé permettant de s’assurer du fonctionnement correct d’un système.
  • Il est dit unitaire s'il ne fait pas appel à d'autres ressources que l'élément testé (et n'utilise pas de base de données, de socket...).
  • Description

L'injection de dépendances

www.flickr.com/photos/clintjcl/1143871796/

L'injection de dépendances

... ce qu'il ne faut pas faire !

class User {
  private $cacheProvider;

  public function __construct() {
    $this->cacheProvider = new CacheProvider();
  }

  public function getUser($userId) {
    if ($this->cache->hasUser($userId)) {
      return $this->cache->getUser($userId);
    }

    return $this->retriveUserById($userId);
  }
}

L'injection de dépendances

... depuis le constructeur

class User {
  private $cacheProvider;

  public function __construct(CacheProviderInterface $cacheProvider) {
    $this->cacheProvider = $cacheProvider;
  }

  public function getUser($userId) {
    if ($this->cache->hasUser($userId)) {
      return $this->cache->getUser($userId);
    }

    return $this->retriveUserById($userId);
  }
}

L'injection de dépendances

... depuis les "setters"

class User {
  private $cacheProvider;

  public function setCacheProvider(CacheProviderInterface $cacheProvider) {
    $this->cacheProvider = $cacheProvider; return $this;
  }

  public function getUser($userId) {
    if ($this->cache->hasUser($userId)) {
      return $this->cache->getUser($userId);
    }

    return $this->retriveUserById($userId);
  }
}

Un peu d'histoire

www.flickr.com/photos/kohlin/3572049890/

PHPUnit

www.flickr.com/photos/akrabat/4639322961/

Atoum

www.flickr.com/photos/arnaudlimbourg/5164535277/

Prêt pour le grand saut ?

www.flickr.com/photos/changgaophotography/11847862934/

La mise en place

www.flickr.com/photos/cubagallery/6135528268/

Installer PHPUnit

Depuis une archive phar :

Avec composer :

  • {
        "require-dev": {
            "phpunit/phpunit": "4.1.*"
        }
    }
    

Dans un projet Symfony2 :

  • Editer le fichier de configuration
  • Déjà fournit avec une configuration minimal !

Installer Atoum

La configuration

www.flickr.com/photos/stavos52093/12485014293/

La configuration

... de PHPUnit

  • Se fait au travers de fichiers XML
  • Rien à faire pour un projet Symfony2
  • Execution des tests :
    phpunit [-c dossier|fichier] dossierDeTests

La configuration

... d'Atoum

  • Tout se fait au travers de fichiers PHP
  • Pré-configuré (pour SF2) avec l'utilisation de l'AtoumBundle
  • Execution des tests :
    atoum [-bf fichierConf] -d dossierDeTests
    app/console atoum
    

L'utilisation

www.flickr.com/photos/jjpacres/3293117576/

Les assertions

... de PHPUnit

De nombreux asserteurs :

  • Types de bases : string, bool, int, float...
  • Classes et Tableaux
  • Fichiers
  • JSON
  • XML
  • Mock

Des annotations :

  • Pour la gestion des exceptions
  • Empêcher l'execution de certains tests

Anatomie d'un test

... PHPUnit

class UserTest extends PHPUnit_Framework_TestCase
{
    public function setUp() { }
    public function tearDown() { }

    public function testConstructor() { 
        // write assertions here ...
    }

    public function testGetUser() {
        // write assertions here ...
    }

    private function createMockCacheProvider() { }
}

Un exemple de test

... PHPUnit

/** @covers Symfony\Component\HttpFoundation\Request::create */
public function testCreate() {
    $request = Request::create('http://test.com/foo?bar=baz');
    $this->assertEquals('http://test.com/foo?bar=baz', $request->getUri());
    $this->assertEquals('/foo', $request->getPathInfo());
    $this->assertEquals('bar=baz', $request->getQueryString());
    $this->assertEquals(80, $request->getPort());

    $request = Request::create('http://test.com/foo', 'GET', array('bar' => 'baz'));
    $this->assertEquals('http://test.com/foo?bar=baz', $request->getUri());
    $this->assertEquals('/foo', $request->getPathInfo());
    $this->assertEquals('bar=baz', $request->getQueryString());
    $this->assertEquals(80, $request->getPort());
}

Les assertions

... d'Atoum

De nombreux asserteurs :

  • Types de bases : string, bool, int, float...
  • Classes et Tableaux
  • Stream et Exception
  • Mock

Des annotations :

  • Empêcher l'execution de certains tests

Une écriture :

  • Chainable
  • Déclarative à la façon d'une user-story

Anatomie d'un test

... Atoum

namespace Vendor\Project\tests\units;

class User extends \atoum
{
    public function setUp() { }
    public function tearDown() { }
    public function beforeTestMethod($method) { }
    public function afterTestMethod($method) { }

    public function testConstructor() { }
    public function testGetUser() { }

    private function createMockCacheProvider() { }
}

Un exemple de test

... Atoum

// FriendsOfPHP/pickle/blob/master/tests/units/Package/XML/Loader.php
public function testLoad()
{
    $this
        ->given(
            $path = FIXTURES_DIR . '/package/pickle.json',
            $loader = new \mock\Composer\Package\Loader\LoaderInterface(),
            $this->calling($loader)->load = $package = new \mock\Composer\Package\PackageInterface()
        )
        ->if($this->newTestedInstance($loader))
        ->then
            ->object($this->testedInstance->load($path))->isIdenticalTo($package)
        ->given($path = uniqid())
        ->then
            ->exception(function() use ($path) {
                $this->testedInstance->load($path);
            })
                ->hasMessage('File not found: ' . $path)
    ;
}

Les adapters

www.flickr.com/photos/deapeajay/2969264395/

Les adapters par l'exemple

  • Sans adapter :
    public function getUserConfiguration($user, $key)
    {
        $configurationFile = file_get_contents($user->getConfigurationFile());
    
        $configuration = $this->parse($configuration);
        return $configuration[$key];
    }
    
  • Avec adapter :
    public function getUserConfiguration($user, $key)
    {
        $configurationFile = $this->fileAdapter->getFileContent($user->getConfigurationFile());
    
        $configuration = $this->parse($configuration);
        return $configuration[$key];
    }
    
    public function setFileAdapter(FileAdapterInterface $fileAdapter) {
        $this->fileAdapter = $fileAdapter;  
    }
    

Les mocks (bouchons)

www.flickr.com/photos/lowgren/7084394535/

Créer un mock

... avec PHPUnit

public function testStub()
{
    $stub = $this->getMockBuilder('MaClasse') // création du bouchon
        ->disableOriginalConstructor()
        ->getMock();

    $stub->expects($this->any()) // configuration du bouchon
        ->method('uneMethode')
        ->will($this->returnValue('uneValeur'));

    $map = [
        ['param1', 'result1'],
        ['paramA', 'paramB', 'paramC', 'result2'],
    ];
    $stub->expects($this->any())
        ->method('doSomething')  
        ->will($this->returnValueMap($map));

    $this->assertEquals('uneValeur', $stub->uneMethode());
    $this->assertEquals('result2', $stub->uneMethode('paramA', 'paramB', 'paramC'));
}

Créer un mock

... avec Atoum

public function testStub()
{
    $controller = new \atoum\mock\controller();
    $controller->__construct = function() {};

    $mock = new \mock\Vendor\Project\MaClasse('param1', $controller);

    $this->calling($mock)->unePropriete = 25;
    $this->calling($mock)->uneMethode = function() { };
    $mock->getMockController()->uneMethode = function() { return 'uneValeur'; };
    
    $this->calling($mock)->methods(function($method) {
        return 'get' === substr($method, 0, 3);
    })->return = uniqid();

    $this
        ->if($mock)
        ->then
            ->string($mock->uneMethode())->isEqualTo('uneValeur')
            ->mock($mock)->call('uneMethode')->once()
                         ->call('getTest')->never();
}

Les tests fonctionnels

www.flickr.com/photos/97337626@N04/9021017466/

Test fonctionnel

... avec PHPUnit (dans Symfony2)

class DemoControllerTest extends WebTestCase
{
    public function testSecureSection()
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/demo/secured/hello/World');
        $crawler = $client->followRedirect();
        
        $form = $crawler->selectButton('Login')->form(array('_username' => 'admin', '_password' => 'adminpass'));
        $client->submit($form);

        $crawler = $client->followRedirect();
        $this->assertCount(1, $crawler->filter('h1.title:contains("Hello World!")'));

        $link = $crawler->selectLink('Hello resource secured')->link();
        $crawler = $client->click($link);

        $this->assertCount(1, $crawler->filter('h1.title:contains("secured for Admins only!")'));
    }
}

Test fonctionnel

... avec Atoum (dans Symfony2)

class BarController extends ControllerTest
{
    public function testGet() {
        $this
            ->request(array('debug' => true))
                ->GET('/demo/' . uniqid())
                    ->hasStatus(404)
                    ->hasCharset('UTF-8')
                    ->hasVersion('1.1')
                ->POST('/demo/contact')
                    ->hasStatus(200)
                    ->hasHeader('Content-Type', 'text/html; charset=UTF-8')
                    ->crawler
                        ->hasElement('#contact_form')
                            ->hasChild('input')->exactly(3)->end()
                            ->hasChild('input')
                                ->withAttribute('type', 'email')
                                ->withAttribute('name', 'contact[email]')
                            ->end()
                            ->hasChild('textarea')->end()
                        ->end();
}}

Les communautés

www.flickr.com/photos/laubarnes/5449810523/

Les communautés

PHPUnit

  • Une communauté mondiale
  • L'outil le plus utilisé
  • Enormément de ressources

Atoum

  • Une communauté essentiellement française
  • Un outil encore relativement peu utilisé
  • Une équipe réactive sur IRC (#atoum)

Avantages / Inconvénients

www.flickr.com/photos/spryng/4431662613/

Récapitulatif

PHPUnit

  • Leader et énorme communauté
  • Intégration dans les IDE (Open Source et propriétaire)
  • Code coverage
  • Nombreux asserters
  • Syntaxe non agile et permissive
  • Pas de contrôle sur les types

Atoum

  • Syntaxe moderne et orienté agile
  • Rapidité d'execution
  • Code coverage
  • Ressources en français
  • Fonctionnement des mocks (avec héritage)
  • Rolling Release
  • PSR
Whenever you are tempted to type something into print statement or a debugger expression, write it as a test instead
Martin Fowler

<Merci !>

Annexe: Code coverage avec PHPUnit

Code coverage avec PHPUnit

Annexe: Code coverage avec Atoum

Code coverage avec Atoum