Studyx - Professional web training

BECOME A WEB EXPERT

Beginnen met PHPUnit

Het internet vandaag …

Vandaag de dag is het heel belangrijk om een mooie, flitsende web site op het internet te hebben. Meestal begint dit met een voorstelling van jezelf, wat je hobbies zijn en hoe mensen het makkelijkst met je in contact kunnen komen. Vooral bedrijven zijn hier erg voor te vinden en het zijn juist die ondernemingen die het internet gevormd hebben tot wat het nu is geworden.

Een doorsnee bedrijf heeft iets te bieden: ze verkopen producten of leveren diensten en willen dit graag op een zo eenvoudige manier mogelijk kunnen doen. Het internet is daar heel geschikt voor. Je zet je producten met foto’s en beschrijvingen online, je adverteert wat via Google of andere kanalen en de eerste klanten komen al wat kopen bij je. Alles gaat vlekkeloos!

Bedrijven zijn ook heel graag vernieuwend en zo’n website wordt dan ook vaak van extra functionaliteiten voorzien. Bij de laatste update loopt er dan toch iets mis: een kleine fout in de berekening van prijzen en mensen krijgen nu wel héél veel korting. En nog voor je het goed en wel in de gaten hebt, schreeuwt iedereen het uit op sociale media zoals Twitter en Facebook dat je nu grandioze kortingen krijgt bij jouw web winkel!

Geloof je niet dat dit gebeurt? Winkelketen Hema heeft het al meegemaakt met hun ‘gratis’ taarten actie. Of wat dacht je van United Airlines die via hun website tickets voor $0 en $10 aanboden. Zelfs ‘grote’ bedrijven maken wel eens een foutje met zeer grote gevolgen.

Unit tests maken het verschil

Om dit soort ongelukken (met grote gevolgen) tegen te gaan, bestaan er verschillende manieren om de broncode te testen. Je kunt eenvoudig kijken naar de website, wat rondklikken en dingen uitproberen (wat eigenlijk iedere web ontwikkelaar een beetje doet), maar je gaat nooit alle mogelijke combinaties kunnen uitproberen want dat gaat jaren kosten.

Een betere oplossing is ‘unit testen’. Een unit is eigenlijk een klein stukje code die juist 1 actie of functionaliteit uitvoert. Dit kan een functie zijn, een conditie of een heel bestand, zolang het maar één enkele verantwoordelijkheid heeft. Dit is belangrijk omdat je dan tests ervoor kunt schrijven die deze functionaliteit langs elke mogelijke kant gaat voeden met goede en slechte waarden. Als ontwikkelaar kun je dan zien of je rekening hebt gehouden met alle mogelijke opties.

Wanneer je dan een verzameling tests hebt geschreven en je moet iets aanpassen, wordt je vanzelf geïnformeerd bij het uitvoeren van deze tests wanneer je iets kapot gemaakt hebt. Want hoe vaak is het al niet voorgevallen dat je iets links aanpast en rechts iets omvalt? Mijzelf al genoeg keren om gemotiveerd aan testen te beginnen.

PHP en PHPUnit

Wanneer je, zoals ik, programmeert in PHP, dan kun je met PHPUnit heel eenvoudig unit tests schrijven.

Een voorbeeldje: functionaliteit die een product, een aantal en de stukprijs ontvangt en het totaal weergeeft in een winkelmandje. De hele class zou er ongeveer zo uit kunnen zien:

    <?php          
    class ShoppingCart          
    {                  
        public $shoppingCart;                                          
    
        public function __construct($shoppingCart = null)                  
        {                          
            if (null === $shoppingCart) {                                  
                $this->shoppingCart = array ();
            }
        }

        public function add($productId, $quantity, $unitPrice)
        {
            $purchase = array (
                'productId' => $productId,
                'quantity' => $quantity,
                'unitPrice' => $unitPrice,
            );
            $this->shoppingCart[] = $purchase;
        }
    
        public function get($index)
        {
            return $this->shoppingCart[$index];
        }
    
        public function getAll()
        {
            return $this->shoppingCart;
        }
    
        public function remove($index)
        {
            unset ($this->shoppingCart[$index]);
        }
    
    }

Laten we even nader kijken naar het toevoegen van een product in het winkelwagentje:

    public function add($productId, $quantity, $unitPrice)
    {
        $purchase = array (
            'productId' => $productId,
            'quantity' => $quantity,
            'unitPrice' => $unitPrice,
        );
        $this->shoppingCart[] = $purchase;
    }

Dus we zien hier dat we een product ID kunnen meegeven, een hoeveelheid en de stukprijs van het product. Dat wordt netjes in een array gestoken en aan de array van het winkelwagentje toegevoegd. Heel eenvoudig en zeker functioneel. Dit is wat we dan een ‘unit’ noemen.

Hoe gaan we dit nu testen? Met PHPUnit! Om ermee te beginnen moet je eerst PHPUnit downloaden, het makkelijkste is om meteen naar de sitefkapp te gaan en op de knop met “latest stable release” te klikken. Werk je met een Mac of Linux computer, kun je het nog makkelijker doen. In je terminal shell voer je het volgende commando uit:

    cd /path/to/codebase
    wget https://phar.phpunit.de/phpunit.phar

En kijk! Je hebt de laatste stabiele versie van PHPUnit. Laten we maar meteen ons eerste testje schrijven!

    <php          
    require_once 'ShoppingCart.php';               

    class ShoppingCartTest extends PHPUnit_Framework_TestCase     
    {                  
        public function testAddProductToCart()         
        {                          
            $shoppingCart = new ShoppingCart;                          
            $shoppingCart->add(1, 1, 4.95);
                
            $entries = $shoppingCart->getAll();
        
            $this->assertCount(1, $entries);
        }
    }

Wat doen we in deze unit test? We kijken of we een product kunnen toevoegen aan ons winkelwagentje.

Als we dan PHPUnit gaan uitvoeren, dan krijgen we een bevestiging dat alles goed gaat. Met het volgende commando voeren we phpunit uit:

    php phpunit.phar ShoppingCartTest ShoppingCartTest.php

En dit geeft PHPUnit terug als bevestiging:

    PHPUnit 4.8.6 by Sebastian Bergmann and contributors.
    
    .
    
    Time: 184 ms, Memory: 13.25Mb
    
    OK (1 test, 1 assertion)

Cool! Onze eerste test en alles werkt! We zijn goed bezig!!!
Laten we onze test nog even uitbreiden om zeker ervan te zijn dat wat we erin hebben gestoken er ook werkelijk in zit.

    <?php          
    require_once 'ShoppingCart.php';       
   
    class ShoppingCartTest extends PHPUnit_Framework_TestCase 
    {                  
        public function testAddProductToCart()                  
        {                          
            $shoppingCart = new ShoppingCart;                          
            $shoppingCart->add(1, 1, 4.95);
                
            $entries = $shoppingCart->getAll();
        
            $this->assertCount(1, $entries);
        
            $expected = array (
                'productId' => 1,
                'quantity' => 1,
                'unitPrice' => 4.95,
            );
            $this->assertSame($expected, $shoppingCart->get(0));
        }
    }

Weer voeren we PHPUnit uit, en krijgen nu wederom een OK terug.

    PHPUnit 4.8.6 by Sebastian Bergmann and contributors.
    
    .
    
    Time: 174 ms, Memory: 13.25Mb
    
    OK (1 test, 2 assertions)

Goh, dit is toch eigenlijk eenvoudig. Maar hoe gaat dit nou ons beschermen tegen fouten?

Wat we hierboven geschreven hebben is testen tegen de normale verwachtingen, wat we al snel “happy testing” noemen. Helaas zijn niet alle gebruikers van je webwinkel zo internet vaardig dat ze alles netjes juist invullen. Fouten gebeuren (per ongeluk of expres) en die kunnen grote gevolgen hebben zoals je in de artikels hierboven hebt kunnen lezen. Want, wat gebeurd er als we een stukprijs van 0 invoeren? Of kunnen we een negatief aantal meegeven? Laten we het maar meteen even testen!

Voeg volgende testjes maar toe in je ‘ShoppingCartTest.php’.

    public function testAddProductWithZeroPrice()
    {
        $shoppingCart = new ShoppingCart;
        $shoppingCart->add(1, 1, 0);
        
        $entries = $shoppingCart->getAll();
        
        $this->assertCount(0, $entries);
    }
    
    public function testAddProductWithNegativeQuantity()
    {
        $shoppingCart = new ShoppingCart;
        $shoppingCart->add(1, -1, 4.95);
        
        $entries = $shoppingCart->getAll();
        
        $this->assertCount(0, $entries);
    }

Wanneer we nu PHPUnit uitvoeren, krijgen we meteen de foutmeldingen te zien.

    PHPUnit 4.8.6 by Sebastian Bergmann and contributors.

    .FF

    Time: 186 ms, Memory: 13.25Mb

    There were 2 failures:

    1) ShoppingCartTest::testAddProductWithZeroPrice
    Failed asserting that actual size 1 matches expected size 0.

    /Users/dragonbe/workspace/studyx/ShoppingCart.php:67

    2) ShoppingCartTest::testAddProductWithNegativeQuantity
    Failed asserting that actual size 1 matches expected size 0.

    /Users/dragonbe/workspace/studyx/ShoppingCart.php:77

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

Eigenlijk is PHPUnit een fijne tool, want niet alleen geeft het aan dat je code niet beschermd is tegen foutieve invoer, maar ook waar het fout gaat in je code. Dus je kunt meteen de aanpassingen maken in ‘ShoppingCart.php’.

    public function add($productId, $quantity, $unitPrice)
    {
        $valid = true;
        if (0 >= $productId || 0 >= $quantity || 0 >= $unitPrice) {
            $valid = false;
        }
        if ($valid) {
            $purchase = array (
                'productId' => $productId,
                'quantity' => $quantity,
                'unitPrice' => $unitPrice,
            );
            $this->shoppingCart[] = $purchase;
        }
    }

Wanneer we nu PHPUnit uitvoeren, krijgen we wel een positieve response terug. Dus hebben we onze aanpassing goed uitgevoerd en zijn we alweer een beetje meer beschermd tegen foutieve invoer.

    PHPUnit 4.8.6 by Sebastian Bergmann and contributors.

    ...

    Time: 185 ms, Memory: 13.25Mb

    OK (3 tests, 4 assertions)

PHPUnit configuratie

Je zult al gemerkt hebben dat wanneer je elke keer het hele commando moet uittypen om PHPUnit uit te voeren, dat niet echt prettig werkt. Gelukkig kunnen we PHPunit configureren, en wel met een XML bestand ‘phpunit.xml’. We zetten dit in de basis directory van onze applicatie omdat we daar ook PHPunit uitvoeren.

Een minimale configuratie is de volgende:

    <?xml version="1.0" encoding="utf-8"?>
    <phpunit>

        <testsuites>
            <testsuite name="ShoppingCart Tests">
                <directory>.</directory>
            </testsuite>
        </testsuites>

    </phpunit>

En nu kunnen we gewoon PHPUnit uitvoeren in de terminal:

    php phpunit.phar

En ook dit geeft ons een succesvol testresultaat terug.

    PHPUnit 4.8.6 by Sebastian Bergmann and contributors.

    ...

    Time: 185 ms, Memory: 13.25Mb

    OK (3 tests, 4 assertions)

Als we phpunit.phar nog even executable maken, kunnen we het met 1 enkel commando doen.

    chmod +x phpunit.phar
    phpunit.phar
    PHPUnit 4.8.6 by Sebastian Bergmann and contributors.
    
    ...
    
    Time: 185 ms, Memory: 13.25Mb
    
    OK (3 tests, 4 assertions)

Nu kunnen we vol gas vooruit en onze winkelwagen beter beschermen.

Optimaliseren van onze tests

Waarschijnlijk heb je al gemerkt dat we heel wat code dupliceren in onze tests. Als goede ontwikkelaar doen we dat niet graag, dus gaan we dat meteen even aanpakken.

PHPUnit heeft twee functies: `setUp()` en `tearDown()` waarin we bij het uitvoeren van elke test enkele zaken kunnen instellen (zoals onze shopping cart) die na afloop weer wordt afgebroken.

Even de volledige ShoppingCartTest.php met de nieuwe methodes toegevoegd:

    <?php          
    require_once 'ShoppingCart.php';          

    class ShoppingCartTest extends PHPUnit_Framework_TestCase          
    {                  
        protected $shoppingCart;                                
        protected function setUp()                  
        {                          
            parent::setUp();                          
            $this->shoppingCart = new ShoppingCart;
        }
    
        protected function tearDown()
        {
            parent::tearDown();
            unset ($this->shoppingCart);
        }
    
        public function testAddProductToCart()
        {
            $this->shoppingCart->add(1, 1, 4.95);
            $entries = $this->shoppingCart->getAll();
            $this->assertCount(1, $entries);
            $expected = array (
                'productId' => 1,
                'quantity' => 1,
                'unitPrice' => 4.95,
            );
            $this->assertSame($expected, $this->shoppingCart->get(0));
        }
    
        public function testAddProductWithZeroPrice()
        {
            $this->shoppingCart->add(1, 1, 0);
            $entries = $this->shoppingCart->getAll();
            $this->assertCount(0, $entries);
        }
    
        public function testAddProductWithNegativeQuantity()
        {
            $this->shoppingCart->add(1, -1, 4.95);
            $entries = $this->shoppingCart->getAll();       
            $this->assertCount(0, $entries);
        }
    }

We hebben al de initialisatie van ons winkelmandje al naar 1 locatie gebracht, maar wat doen we nu met onze tests voor foute invoer? We kunnen beroep doen op de speciale annotatie `@dataProvider` die we voor een test kunnen schrijven.

De volgende code gaat `testAddProductWithZeroPrice()` en `testAddProductWithNegativeQuantity()` functies vervangen:

    public function badDataProvider()
    {
        return array (
            array (1, 1, 0),
            array (1, -1, 4.95),
            array (0, 1, 4.95),
        );
    }
    
    /**
     * @dataProvider badDataProvider
     */
    public function testProtectCartInput($id, $qty, $price)
    {
        $this->shoppingCart->add($id, $qty, $price);
        $entries = $this->shoppingCart->getAll();
        $this->assertCount(0, $entries);
    }

Wanneer we nu PHPUnit uitvoeren, krijgen we wederom een succesvol testresultaat terug.

    PHPUnit 4.8.6 by Sebastian Bergmann and contributors.

    ....

    Time: 196 ms, Memory: 13.25Mb

    OK (4 tests, 5 assertions)

Wat leuk is aan de `dataProvider` is dat we nu nog meer test scenario’s kunnen bedenken waartegen we ons moeten gaan beschermen. Bijvoorbeeld wat zou er gebeuren als iemand ‘fiets’ ingeeft in plaats van het aantal? Laten we gewoon testen, we moeten het gewoon aan onze `badDataProvider()` toe te voegen.

    public function badDataProvider()
    {
        return array (
            array (1, 1, 0),
            array (1, -1, 4.95),
            array (0, 1, 4.95),
            array (1, 'fiets', 4.95),
        );
    }

Wanneer we PHPUnit uitvoeren, zien we dat we er tegen beschermd zijn.

    PHPUnit 4.8.6 by Sebastian Bergmann and contributors.

    .....

    Time: 182 ms, Memory: 13.25Mb

    OK (5 tests, 6 assertions)

Eindeloos veel mogelijkheden

PHPUnit is mijn tool bij uitstek om te testen maar ook om te debuggen gezien het mij meteen tot de fout brengt. Maar ook om veiligheid te verbeteren door bijvoorbeeld op een security sites codes van hackers te kopiëren en aan mijn `dataProvider` toe te voegen.

Waar moet je beginnen met testen? Ga kijken in je code wat de belangrijkste onderdelen zijn. Waar ga jij of je bedrijf geld mee verliezen wanneer het omvalt? Dat is wat je zoveel mogelijk wilt beschermen met unit tests.

Ik zou zeker aanraden om de voorbeelden op phpunit.de te bekijken en te volgen. Want er is nog zoveel meer wat PHPUnit kan, het is gewoon niet samen te vatten in één artikel.

Heb je interesse in een opleiding in PHPUnit? Aarzel dan niet om contact met ons op te nemen. Of schrijf je in op onze PHPUnit opleiding.

Share on FacebookShare on LinkedInTweet about this on TwitterShare on Google+Email this to someonePrint this page
02/09/15

0 Reacties opBeginnen met PHPUnit"

Laat een bericht na