Automated testing

Manueel software testen is duur en foutgevoelig. Gelukkig kan je dit proces ook automatisch laten verlopen. Hierbij kan je verschillende aspecten binnen de applicatie gaan testen zoals functionaliteit, performantie, beveiliging, gebruikerservaring (UX), efficientie (A/B), enzovoort.

Maar een test kan vaak ook op verschillende niveaus gebeuren, van de kleinste eenheid tot een end-to-endtest.

Wat testen?

Het leven zit vol risico’s, en zo ook een softwareproject. Wat er precies getest moet worden en hoe diepgaand, is vaak een beslissing van de business op basis van hoe bedrijfskritisch de software is. Bij een nieuw project kan je meteen geautomatiseerde testen toevoegen. Bij een bestaand project is dit soms moeilijker, maar het is wel de beste manier om legacy onder controle te krijgen.

De business verwacht vaak van development dat de belangrijkste problemen zo snel mogelijk opgespoord worden, aan een zo laag mogelijke kost. Zo heeft het weinig nut om voor een bepaalde situatie een test toe te voegen, als het onwaarschijnlijk is dat de situatie zich ook echt kan voordoen.

Een andere factor die in rekening gebracht moet worden is de hoeveelheid schade die kan worden aangericht. Dit kan een financiële kost zijn, verlies van vertrouwen, reputatieschade, de reparatiekosten, etc. Als er geen risico op schade is, is er ook geen nood aan een test.

Een applicatie 100% dekken met automatisch testen is vaak niet haalbaar. Een gezonde afweging van kosten en baten is hier aan de orde. Denk je er trouwens ook aan om niet alleen PHP maar ook JavaScript, CSS, database queries, ... te testen? Wordt de infrastructuur ook getest?

Unit testing

Op het laagste niveaus kan je unit tests toevoegen. In zo'n test wordt de kleinst mogelijke testbare eenheid, bv. een PHP-functie binnen de applicatie, getest. Veel programmeertalen hebben een unit testing implementatie volgens de xUnit architectuur, gebaseerd op de oorspronkelijke SUnit (Smalltalk) implementatie. In PHP heet die implementatie PHPUnit.

De xUnit architectuur bevat een test runner (executable) die een reeks test suites zal uitvoeren. Een testsuite bevat een reeks testcases waarvoor via zogenaamde fixtures een bepaalde context wordt klaargemaakt. Binnen die context worden de tests uitgevoerd. Via het uitvoeren van assertions wordt geverifieerd dat de te testen eenheid zich gedraagt zoals het hoort. De testsuite met testcases, fixtures en assertions moet je als programmeur zelf opbouwen.

Hieronder een voorbeeld van het uitvoeren van een PHPUnit test suite. Elk puntje is een succesvol uitgevoerde unit test met een‎ of meerdere assertions.

phpunit.png

Voor een e-commerce project waar we ook de picking van orders automatiseren hebben we zo bijvoorbeeld een test om ervoor te zorgen dat er geen te grote of te zware order items naar een sorteermachine gestuurd worden. De test ziet er als volgt uit:


class ProductEvaluationServiceTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider orderProvider
     */
    public function test_should_detect_orders_to_exclude_from_sorter(
        $expectedCanBeProcessedBySorter,
        $expectedWeight,
        $expectedVolume,
        $tooHeavy,
        $tooLarge,
        $orderedProducts
    ) {
        $productRepoMock = $this->getMockBuilder('\Product\Repository')->getMock();
        $evaluator = new ProductEvaluationService($productRepoMock);
        $this->assertEquals(
            $expectedCanBeProcessedBySorter,
            $evaluator->orderedProductsCanBeProcessed($orderedProducts)
        );
        $this->assertEquals(
            $expectedWeight,
            $evaluator->calculateProductsWeight($orderedProducts)
        );
        $this->assertEquals(
            $expectedVolume,
            $evaluator->calculateProductsVolume($orderedProducts)
        );
        $this->assertEquals(
            $tooHeavy,
            $evaluator->isOrderTooHeavy($orderedProducts)
        );
        $this->assertEquals(
            $tooLarge,
            $evaluator->isOrderTooLarge($orderedProducts)
        );
    }
...
}

De dataprovider genaamd "orderProvider" is een helpfunctie die de "test_should_detect_orders_to_exclude_from_sorter" voorziet van een aantal voorgedefinieerde orders om mee te testen.

Moest er ooit een case zijn waar de sorteermachine op zou falen, dan kunnen we deze case makkelijk toevoegen aan de orderProvider. De tests zouden dan falen voor de nieuwe case. Vervolgens kan de implementatie gecorrigeerd worden zodat de tests terug lukken.

De testsuite fungeert als vangnet voor de developer bij het refactoren. Omdat de evaluator onder tests is geplaatst kan hier makkelijk aan gewerkt worden zonder dat de developer zich zorgen moet maken de bestaande use cases te breken.

Je kan ook voor elke nieuwe specificatie eerst een test schrijven en de implementatie schrijven op basis van deze test. Dit test-first process heet test-driven development of TDD. Over de bewering dat dit een beter design zou opleveren, en dus best practice is, liggen de meningen uiteen.

red-green-refactorFINAL2.png

PHPUnit kan code coverage rapporteren. Dit is een metric die vaak fout geïnterpreteerd wordt. Code coverage is handig om te vinden voor welke delen van een applicatie geen tests voorzien zijn, maar ga er niet van uit dat dit feit ook omgedraaid kan worden. De code coverage rapporteert namelijk enkel waar de testsuite gepasseerd is op een bepaalde plaats - dit wil niet zeggen dat er ook getest werd.

100% coverage wil dus absoluut niet zeggen 100% getest. Het is daarom beter om het percentage van code coverage niet te gebruiken om de kwaliteit te meten. Een metric die relevanter is om te weten of je voldoende tests hebt, is het aantal bugs die ontdekt worden.

Enkel vertrouwen op unit tests is niet voldoende. Onderstaand voorbeeld toont dit mooi aan.

BDD

Naast de TDD aanpak bij unit testing bestaat er ook een gelijkaardige behavior-driven design of BDD aanpak. BDD combineert technieken en principes van TDD met ideeën uit domain-driven design (DDD). Binnen BDD kan je tests laten baseren op specificaties, de zogenaamde SpecBDD, of op user stories, de zogenaamde StoryBDD.

Bij SpecBDD bestaat de techniek er uit om telkens het gedrag van de objecten te beschrijven vooraleer je deze gaat implementeren. Vervolgens wordt er dan net voldoende code geschreven om aan de specificatie te voldoen. De code kan zodoende veilig gerefactored worden tot een clean design om vervolgens weer verder te gaan met de volgende stap.

Deze werkwijze wordt in kleine iteratieve stapjes toegepast tot het gewenste resultaat bekomen is; precies dezelfde aanpak als bij TDD. Een tool om aan SpecBDD te doen in PHP is phpspec.

phpspec.png

Acceptance testing

Het is even belangrijk om het gedrag van de individele units binnen het grotere geheel te testen. Een manier om dat te doen is door specifieke scenario's binnen een use-case te gaan testen.

StoryBDD is een manier om testing te doen op basis van use-cases. Je schrijft scenario's voor een bepaalde feature in een testbestand en kan deze scenario's automatisch door een tool laten uitvoeren. De tool gaat dan na of de applicatie zich gedraagt zoals vooropgesteld in de acceptatiecriteria.

Populaire tools hiervoor in de PHP wereld zijn Behat en Codeception.

behat.png

Je kan bij het testen van scenario's de focus leggen op het technische aspect. Bijvoorbeeld "When I click '#the-list tr:first-child .row-title'", kan voor jou als developer voldoende duidelijk zijn.

Een voordeel van BDD te gebruiken met Behat is echter dat er gebruik wordt gemaakt van een domain specific language (DSL) genaamd Gherkin. Deze taal is ook door niet-technische mensen leesbaar. Dit maakt het mogelijk om samen met de business test scenario's op te stellen. Er is geen vertaalslag nodig naar de technische kant. De tests zullen hierdoor beter aansluiten bij de definitie van het problem domain en we kunnen gebruik maken van de "ubiquitous language" uit domain driven design.

Tests met Behat of Codeception hebben ook de mogelijkheid om JavaScript uit te voeren. Deze tools integreren hiervoor bij uitstek met Selenium. Indien er voor een bepaald scenario JavaScript-functionaliteit nodig is, zal er een echt browservenster geopend worden en zal Selenium ervoor zorgen dat de stappen uit het scenario in de browser uitgevoerd worden.

Je kan Selenium ook headless draaien, dit wil zeggen op een server zonder display manager. Selenium ondersteunt ook setups met meerdere devices en browsers. Het is wel een hele klus om zo'n selenium grid met verschillende devices en browsers te onderhouden enkel om Selenium testing uit te voeren. Oplossingen als BrowserStack bieden daarom Selenium aan in de cloud waar je gebruik kan maken van meer dan 700 echte desktop- en mobiele browsers.

Een volledige acceptance testing suite uitvoeren duurt vaak een tijdje. Je zal in je deployment process hier rekening mee moeten houden. Een goede continuous delivery-aanpak helpt daarbij.

Continuous_Delivery_process_diagram.svg.png

(Bron https://en.wikipedia.org/wiki/Continuous_delivery#/media/File:Continuous_Delivery_process_diagram.svg)

Performance testing

Of je website wel de vooropgestelde bezoekersaantallen aan zal kunnen en niet door zijn knieën gaat op piekmomenten, kan je te weten komen via performance en load testing.

Een simpele test zou kunnen zijn: hoe vaak kan ik de homepage opvragen op een minuut tijd? Dit soort test kan je met apachebench (ab) uitvoeren (onderdeel van apache2-utils). Dit is natuurlijk geen realistische workload.

Om een goede simulatie te maken van real world traffic kan je met een tool als Apache JMeter aan de slag. Hier kan je ook weer volledige scenario's opbouwen en configureren hoe vaak deze in de load test aan bod moeten komen. JMeter kan geleidelijk meer gebruikers in je systeem brengen tot je in de output duidelijk merkt dat de website aan zijn maximum zit.

De resultaten van JMeter kan je op verschillende manieren verwerken: weergeven in een grafiek of een boomstructuur met resultaten per test, exporteren naar CSV voor verdere analyse in Excel, ...

jmeter.png

Op een lager niveaus bottlenecks vinden en oplossen kan je dan weer met Blackfire. Met deze tool kan je onder meer visueel de performance van een code path vergelijken tussen 2 commits. Je krijgt details over het verschil in wall time, I/O, CPU en memory usage, SQL queries etc. Bovendien kan je dit soort tests oproepen in je build scripts.

Blackfire is de beste profiler die voor PHP op dit moment beschikbaar is.

blackfire_diff.png

Security testing

Wil je niet met een gehackte site eindigen, dan is het installeren van security updates essentieel. Maar je kan er niet automatisch van uitgaan dat die updates alle security issues oplossen. Er zit vaak ook custom code in een project of er kunnen configuratiefouten op servereniveaus gemaakt zijn.

Ook security kan automatisch getest worden met een security scanner. Er zijn verschillende soorten scanners die op verschillende niveaus's kunnen werken. Zo zijn er scanners op broncode niveaus, scanners op web niveaus, netwerk niveaus, ...

Een automatische scan genereert een statusrapport over het veiligheidsniveaus van de website met eventuele risico’s. Scanners kunnen op een snelle en eenvoudige manier issues aan het licht brengen waar via een manuele audit veel tijd in zou kruipen.

Scanners zullen minstens de top issues van OWASP controleren. Het doel is de open deuren te vinden, maar verwacht er ook niet alles van. Scanners identificeren vooral het low-hanging fruit.

Security issues in populaire CMS-systemen worden op het internet massaal misbruikt. Security updates installeren is, zoals reeds gezegd, essentieel. Security scanners als Acunetix hebben ook specifieke controles voor bijvoorbeeld Drupal aan boord om bekende problemen ook meteen te controleren en rapporteren.

acunetix.png

Referenties

Intracto geeft regelmatig hands-on trainingen vol praktische tips zodat je zelf aan de slag kan. We richten ons op kleine groepen van max. 10 deelnemers.

Bekijk onze eerstvolgende opleidingen of vraag een training aan indien deze niet in het aanbod staat.