We plaatsen een cookie voor Google Analytics om onze website te verbeteren

Met een cookie kun je advertenties personaliseren. Wij hanteren echter de strikte regels van de Autoriteit Persoonsgegevens. Surfgedrag houden we niet bij en we achtervolgen je ook niet met reclame.

Een crash course software testen!

Het probleem van slecht functionerende software is iedereen bekend, maar wat is de oplossing? Software testen is de meest voor de hand liggende oplossing en dat is precies waar we in dit blog naar gaan kijken. Laten we beginnen!

Een crash course software testen!

Software testen: de voor- en nadelen

Testen is typisch gebaseerd op het controleren van aannames. Dat is wat het merendeel van de softwarewereld nu gebruikt en ook wat we in dit blog gaan bekijken. Het voordeel van deze aanpak is dat het gemakkelijk is te begrijpen, waar het nadeel ervan is dat het lastig is om zo alle randgevallen te vinden. Daarnaast kost het nog altijd veel tijd, maar bespaart het je wel frustratie als het goed wordt gedaan.

Een geheel andere aanpak is de formele methode waarbij je een (wiskundig) bewijs levert. Het grote voordeel van deze aanpak is dat je alles doorrekent waardoor je ook de randgevallen meepakt. Helaas is deze aanpak een stuk moeilijker waardoor formele bewijzen vooral te vinden zijn in de academische wereld en omgevingen met zeer hoge eisen qua betrouwbaarheid.

Wij gaan ons richten op praktisch inzetbaar testen. Dat doen we door een realistisch gebruiksvoorbeeld te pakken, een zogeheten ‘use case’, en daar een minimaal voorbeeld uit te extraheren. Onze recent geïntroduceerde proactieve monitoring is een mooie use case en neem ik daarom in dit blog als voorbeeld.

Onze code

Voor onze klanten willen we een dienst gaan opzetten waar klanten geïnformeerd worden als hun gebruik hoger ligt. Daarvoor hebben we enkele simpele functies geschreven en een complexere functie (‘business logic’) die alles combineert tot een nuttig iets.

class Mailer {
    public function send($to, $subject, $message) {
        return mail($to, $subject, $message);
    }
}

function getCustomers() {
    return array('henk@example.com');
}

function getUsage($customer) {
    return 1;
}

function shouldNotifyCustomer($customer, $usage) {
    return $customer == "henk@example.com" && $usage > 0.75;
}

function notifyCustomer($mailer, $customer, $usage) {
    return $mailer->send($customer, "High usage", "Your usage is high: ${usage}");
}

function checkUsageAndNotify($mailer) {
    $customers = getCustomers();

    foreach($customers as $customer) {
        $usage = getUsage($customer);
        if (shouldNotifyCustomer($customer, $usage)) {
            notifyCustomer($mailer, $customer, $usage);
        }
    }
}

Nu willen we zeker weten dat onze dienst goed werkt, dus we gaan testen. Ons voorbeeld is in PHP en dan ligt PHPUnit het meest voor de hand. Dit gaan we ook in onze verdere voorbeelden gebruiken.

PHPUnit

Het ontwerp van PHPUnit is gebaseerd op de xUnit-familie. Dat betekent dat Java-ontwikkelaars veel van JUnit zullen herkennen. Het heeft ook veel overeenkomsten met Python unittest. Allen werken ze met een klasse waarop je testfuncties schrijft. Die gebruiken weer ‘assertions’ om te testen of aannames kloppen. Voorbeelden van deze aannames zijn assertTrue, assertFalse en assertEquals. Zolang de aanname klopt, is er niets aan de hand. Als het faalt, dan krijg je een melding waar het is misgegaan. Een aanroep kan er als volgt uitzien:

$ phpunit tests
PHPUnit 5.4.4 by Sebastian Bergmann and contributors.

F.....                                    6 / 6 (100%)

Time: 113 ms, Memory: 4.00MB

There was 1 failure:

1) ShouldNotifyCustomerUnitTest::testHenkWithHighUsage
Failed asserting that true is false.

/app/tests/notificationTest.php:4

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

Hier zien we dat een assertion faalt en ons wordt verteld welke regel in de test dat is, wat je aanknopingspunt is om verder te gaan zoeken. Met deze basiskennis gaan we ‘notificationTest.php’ vullen.

Unit testen

Het meest gedetaileerde niveau van testen is ‘unit testen’. Hierbij probeer je typisch een enkele functie te testen. Dit heeft als voordeel dat je precies weet waar het mis is gegaan als de test faalt. Het nadeel is dat het heel erg vaak als dubbel werk voelt.

Wat we willen bereiken, is met een paar gevallen kijken of er wel of geen notificatie verstuurd zou worden. Dit kunnen we met de volgende test bereiken:

class ShouldNotifyCustomerUnitTest extends PHPUnit_Framework_TestCase {
    public function testHenkWithHighUsage() {
        $this->assertTrue(shouldNotifyCustomer("henk@example.com", 0.76));
    }

    public function testHenkWithLowUsage() {
        $this->assertFalse(shouldNotifyCustomer("henk@example.com", 0.75));
    }

    public function testOtherWithHighUsage() {
        $this->assertFalse(shouldNotifyCustomer("other@example.com", 0.76));
    }

    public function testOtherWithLowUsage() {
        $this->assertFalse(shouldNotifyCustomer("other@example.com", 0.75));
    }
}

Zoals je kan zien, testen we hier shouldNotifyCustomer met vier gevallen waarvan we weten dat ze wel of juist niet gemaild moeten worden. Dit resulteert in veel meer code dan de oorspronkelijke functie, maar als iemand per ongeluk iets stukmaakt dan weet je dat wel meteen. Bijvoorbeeld als ook Peter gemaild moet worden, dan kun je de implementatie aanpassen naar:

function shouldNotifyCustomer($customer, $usage) {
    return $customer == "henk@example.com" || $customer == "peter@example.com" && usage > 0.75;
}

Als goede ontwikkelaar draaien we direct onze tests en zien dat testHenkWithLowUsage faalt. Blijkbaar krijgt Henk met een laag gebruik ook een e-mail, terwijl we dat niet zouden verwachten! Het is aan de lezer om te bepalen waar de fout zit.

Het is een goed gebruik om zo min mogelijk assertions per functie te gebruiken, zodat je tests heel specifiek blijven. Als je dat niet doet en meer tegelijk gaat testen, dan loop je het risico dat je complete ’test suite’ omvalt bij een kleine aanpassing waardoor je alsnog alle code moet gaan lezen.

Een crash course software testen! Testing in progress

De notifyCustomer functie te testen, is iets complexer, omdat we tijdens het testen niet willen mailen. Met een ‘stub’ kun je het gedrag opgeven zonder de daadwerkelijke acties uit te voeren. Met Dependency Injection geven we onze stub mee.

class NotifyCustomerUnitTest extends PHPUnit_Framework_TestCase {
    public function testMailIsSent() {
        $stub = $this->getMockBuilder('Mailer')->getMock();
        $stub->method('send')->willReturn(true);
        $this->assertTrue(notifyCustomer($stub, 'user@example.com', 0.8));
    }
}

Wellicht verbaast het je dat we een ‘mock builder’ gebruiken om een stub te bouwen. Hier komen we later op terug.

Integration testing

In een typisch ontwerp combineer je simpele functies tot modules. Het testen hoe deze modules werken en hoe de verschillende functies samenwerken, wordt gedaan met met zogeheten ‘integratietests’. In onze code zit de integratie in checkUsageAndNotify dus we gaan een CheckUsageAndNotifyIntegrationTest schrijven. Hierin willen we controleren of de juiste e-mails verstuurd zouden zijn.

class CheckUsageAndNotifyIntegrationTest extends PHPUnit_Framework_TestCase {
    public function testIntegration() {
        $mock = $this->getMockBuilder('Mailer')
                     ->setMethods(array('send'))
                     ->getMock();

        $mock->expects($this->once())
             ->method('send')
             ->with('henk@example.com', 'High usage', 'Your usage is high: 1');

        checkUsageAndNotify($mock);
    }
}

Om deze functie te testen, hebben we een ‘mock’ gebruikt. Het verschil tussen een ‘stub’ en een ‘mock’ is dat je een stub vertelt wat er gedaan moet worden, terwijl je met een mock controleert of de juiste actie gedaan is. In ons geval controleren we of de mailer gevraagd is om een e-mail te sturen aan de juiste persoon met de juiste inhoud. Daarna roepen we onze te testen functie aan. Aan het einde van de functie controleert PHPUnit voor ons of aan de verwachtingen voldaan is.

Merk op dat in ons voorbeeld vanwege de lengte getCustomers en getUsage triviale functies zijn, maar in werkelijkheid zou je waarschijnlijk een ‘CustomerService’ en ‘UsageService’ klassen hebben die je hier zou ‘stubben’. Dat zou echter meer van hetzelfde moeten zijn.

System testing

Bij ‘system testing’ ga je testen of alle losse componenten op de juiste manier met elkaar praten. Dit valt binnen ‘black-box testing’ waarbij de interne werking onbelangrijk is. Alleen wat er ingaat en wat er uitkomt is van belang.

Een crash course software testen! Black-box testing

In onze use case kan dat zich vertalen naar een klant aanmaken en zorgen voor een hoog gebruik. Je verwacht nadat het systeem zijn werk heeft gedaan dat er een e-mail in de mailbox van de klant terecht komt. Het mag voor zichzelf spreken dat dit veel te uitgebreid is om hier uit te werken.

Het is echter ook mogelijk om een browser aan te sturen en rond te laten klikken in een browser. Het volgende voorbeeld is gebaseerd op Working with PHPUnit and Selenium Webdriver, maar lees het vooral na voor het opzetten van Selenium en de browser.

class AntagonistTest extends PHPUnit_Framework_TestCase {
    protected $webDriver;

    public function setUp() {
        $capabilities = array(\WebDriverCapabilityType::BROWSER_NAME => 'firefox');
        $this->webDriver = RemoteWebDriver::create('http://localhost:4444/wd/hub', $capabilities);
    }

    public function testGitHubHome() {
        $this->webDriver->get('https://www.antagonist.nl');
        $this->assertContains('Antagonist', $this->webDriver->getTitle());
    }
}

Deze test zal Firefox starten, naar https://www.antagonist.nl gaan en controleren of ‘Antagonist’ in de titel staat. Uiteraard kun je ook formulieren invullen en de inhoud van de pagina gebruiken. Hiermee simuleer je dus echte gebruikers!

Hoe verder?

In onze crash course hebben we een tipje van de sluier opgelicht. We hebben daarbij de verschillende types van testen bekeken. Ook zijn stubs en mocks kort aan bod gekomen, maar er is meer gereedschap. Zo zijn er fixtures, manieren om tests over te slaan en vele assertion functies. Lees dit rustig door want het kan je veel tijd schelen.

Een crash course software testen! De verschillende manieren van testen

Zolang je tests niet uitvoert, is al het werk natuurlijk voor niets. De gebruikelijke oplossing is Continuous Integration. Op ekohl/notification-example staat alle gebruikte code inclusief een .travis.yml bestand in waarmee Travis CI wordt gebruikt. Dit is een begin, maar de mogelijkheden rondom CI verdienen een blog op zich.

Werken bij Antagonist

Is dit alles voor jou een koud kunstje? Neem dan eens een kijkje bij de vacatures op onze website en stuur ons een mailtje! We zijn hard op zoek naar nieuwe collega’s om ons team mee te versterken.

Bekijk de vacatures →

P.S. Wil je op de hoogte blijven van alle artikelen, updates, tips en trucs die verschijnen op ons blog? Dat kan! Via RSS, per e-mail, het liken op Facebook, het +1’en op Google+ of het volgen op Twitter.

Deel dit blog
Ewoud Kohl van Wijngaarden
Ewoud Kohl van Wijngaarden

Ewoud versterkt sinds 2014 het development-team. Naast ontwikkelen is systeembeheren ook een iets te serieuze hobby geworden. Je mag Ewoud dus gerust als techneut omschrijven, mede door zijn passie open source.

Artikelen: 7

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *

Sterren Webhosting: 5 sterren uit 5.830 reviews

60.000+ webhostingpakketten actief
Bij de beste webhosters in MT1000 en Emerce 100