PHPUnit

Test unitaires #

Règles #

  • A voir au moins autant de tests qu’autant de nombre de return et d’exception pour une fonction donnée

  • Les tests doivent s’exécuter le plus rapidement possible

  • Les tests doivent être isolés les uns des autres

  • Les tests doivent être répétable à l’infini et toujours renvoyer OK

  • Les tests doivent se suffirent à eux-mêmes pour être compréhensible aux autres développeurs

  • Couvrir le maximum de cas pour permettre l’anticipation

  • Step by step example

En quoi consiste l’écriture d’un test avec phpunit? #

  • Instancier une classe
  • Appeler une méthode
  • En vérifier la sortie

Couverture de code #

  • La couverture de code est une mesure qui permet d’identifier la proportion du code testé
  • La couverture de code est une mesure du nombre de lignes/blocs/arcs de votre code exécutés pendant l’exécution des tests automatisés.
phpunit --coverage-html web/test-coverage

Configurations #

Fichier: phpunit.xml.dist

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
    backupGlobals="false"
    colors="true"
    bootstrap="vendor/autoload.php"
>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./src</directory>
        </include>
    </coverage>

    <testsuites>
        <testsuite name="Test Suite">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

Comment faire pour afficher le code coverage à chaque qu’on lance la commande phpunit?

<logging>
    <log type=”coverage-text” target=”php://stdout” />
</logging>

Data providers #

class ProductTest extends TestCase
{
    /**
     * @dataProvider pricesForFoodProduct
     */
    public function testcomputeTVAFoodProduct($price, $expectedTva)
    {
        $product = new Product('Un produit', Product::FOOD_PRODUCT, $price);

        $this->assertSame($expectedTva, $product->computeTVA());
    }

    public function pricesForFoodProduct()
    {
        return [
            [0, 0.0],
            [20, 1.1],
            [100, 5.5]
        ];
    }
}

Doublure #

  • Elément qu’on créé de toutes pièces pour maîtriser une dépendance externe
  • Le principe d’un test unitaire est surtout de ne pas dépendre d’un système externe

MOCK #

  • Objet créé à partir d’un type de classe

  • Le but est d’effectuer tranquillement vos tests unitaires sur une méthode qui a besoin de ce type de classe

  • Dummy: Objet un peu particulier qui remplit un contrat

  • Stub: Dummy auquel on ajoute un comportement

  • Mock: Un Stub qui a des attentes

class ClassTest extends TestCase
{
    public function testExemple()
    {
        $request = new Request();

        // Ceci est un Dummy
        $client = $this->getMock('GuzzleHttp\Client');

        // Ceci est un Stub
        $client->method('get')->willReturn($request);

        // Ceci est un Mock
        $client
            ->expects($this->once()) // Signifie que la méthode ne sera appelée qu'une seule fois
            ->method('get')
            ->willReturn($request);

        // …
    }
}

Use cases #

1. Un objet est difficile à instancier

class Serializer
{
    public function __construct(
        MetadataFactoryInterface $factory,
        HandlerRegistryInterface $handlerRegistry,
        ObjectConstructorInterface $objectConstructor,
        MapInterface $serializationVisitors,
        MapInterface $deserializationVisitors,
        EventDispatcherInterface $dispatcher = null,
        TypeParser $typeParser = null,
        ExpressionEvaluatorInterface $expressionEvaluator = null
    )
    {}
}

class ExempleClassTest extends TestCase
{
    public function testExemple()
    {
        $serializer = $this
            ->getMockBuilder('JMS\Serializer\Serializer')
            ->disableOriginalConstructor()
            ->getMock();

        $classToTest = new ExempleClass($serializer);
        // …
    }
}

2. Maîtriser le retour d’une méthode appelée par le code original

class GithubUserProvider extends UserProviderInterface
{
    private $client;

    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    public function loadUserByUsername($username)
    {
        $response = $this->client->get('https://api.github.com/user?access_token='.$username);

        // …
    }
}

class GithubUserProviderTest extends TestCase
{
    public function testLoadUserByUsername()
    {
        $response = 'Guzzle retournera TOTO'; // Ce que l'on souhaite recevoir.

        $client = $this->getMockBuilder('GuzzleHttp\Client')
            ->disableOriginalConstructor()
            ->setMethods(['get'])
            ->getMock();
        $client
            ->method('get')
            ->willReturn($response);

        $githubUserProvider = new GithubUserProvider($client);
        $githubUserProvider->loadUserByUsername('xxxxx');

        // Assertions du test
        // …
    }
}

SetUp #

On y met les instructions qu’on veut exécuter avant chaque test

    public function setUp()
    {
        $this->client = $this->getMockBuilder('GuzzleHttp\Client')
            ->disableOriginalConstructor()
            ->setMethods(['get'])
            ->getMock();

        $this->serializer = $this
            ->getMockBuilder('JMS\Serializer\Serializer')
            ->disableOriginalConstructor()
            ->getMock();

        // ...
    }

tearDown #

On y met les instructions qu’on veut exécuter à chaque fin de test

    public function tearDown()
    {
        $this->client = null;
        $this->serializer = null;
        $this->streamedResponse = null;
        $this->response = null;
    }