"Zou moeten werken"

Door E-sites, E-sites
15 maart 2012 - 831 x bekeken -

Hier bij E-sites vinden we kwaliteit belangrijk; wanneer we iets opleveren, moet het gewoon werken. Om daar zeker van te zijn, moet je testen. Iedere feature, bij iedere wijziging.

Maar testen kost tijd en is daarnaast erg saai werk. En tja, dat ik een veldje heb toegevoegd aan het contactformulier heeft toch geen invloed op de paginering van het nieuws overzicht? Dus dan is het ook niet echt nodig om te uitgebreid te gaan testen toch? Dit leidt tot wat wij hier kennen als “zou moeten werken”: Er is geen enkele reden waarom een bepaalde wijziging ergens invloed op zou moeten hebben en dus kan het eigenlijk niet dat het niet meer werkt, maar 100% zeker weten dat het nog werkt doen we niet. En in 99% van de gevallen gaat dat goed. Maar zoals ik al zei: hier bij E-sites vinden we kwaliteit belangrijk. Dus die 1% van de gevallen waarin dat niet goed gaat, vinden wij niet acceptabel.

Één optie is om toch alles iedere keer handmatig te gaan testen. Daar wordt je als ontwikkelaar niet blij van, maar ook als klant niet – die tijd moet immers betaald worden.

Maar je kunt die tests natuurlijk ook automatiseren. Door een hele reeks automatische tests neer te zetten (en in je CI server te hangen) heb je veel meer zekerheid over het juist functioneren van een project, ook wanneer je een half jaar later aanpassingen moet doen.

Hierin maken we bij E-sites gebruik van 2 technieken: unit testing en integration/acceptance testing. Als backend developer vind ik vooral de eerste interessant, dus ga ik het daar over hebben.

Als we Wikipedia vragen wat unit testing is dan is het antwoord "unit testing is a method by which individual units of source code are tested to determine if they are fit for use. A unit is the smallest testable part of an application”. Je gaat dus je code opbreken in zo klein mogelijk delen en deze allemaal los testen. Belangrijk hierbij is om je units of code te isoleren van alle afhankelijkheden. Niet alleen zie je hierdoor veel sneller waar precies een probleem zit, de tests worden er ook veel eenvoudiger door.

Hierbij verstaan we onder afhankelijkheden niet alleen externe diensten, maar ook de eigen database en zelfs andere onderdelen van het systeem. Wanneer ik bijvoorbeeld een test heb voor het registreren van een account, wil ik niet dat deze faalt door een probleem in het email validatie component[1]. Wat ik wel wil weten, is dat dat component gebruikt wordt – voor het succesvol registreren van een account is immers een valide email adres nodig.

Om deze afhankelijkheiden te omzeilen gebruiken we mocks en dit ziet er ongeveer zo uit:

/** 
 * @covers AccountService::registerAccount 
 */ 
public function testRegisterAccount() 

    
//    Create mock validator 
    
$oValidatorMock $this->getMock('Validator', array('email''username')); 

    
//    Expect Validator->email('jory@E-sites.nl') 
    
$oValidatorMock->expects($this->once()) 
        ->
method('email'
        ->
with($this->equalTo('jory@E-sites.nl')); 

    
//    Expect Validator->username('jgeerts') 
    
$oValidatorMock->expects($this->once()) 
        ->
method('username'
        ->
with($this->equalTo('jgeerts')); 

    
// Inject the validator into the account service. 
    
$this->object->setValidator($oValidatorMock); 

    
//    Create a dummy account. 
    
$oAccount = new Account(); 
    
$oAccount->setEmail('jory@E-sites.nl'); 
    
$oAccount->setUsername('jgeerts'); 

    
//    Do the actual test 
    
$this->object->registerAccount($oAccount); 
}

In normaal Nederlands: We verwachten dat, wanneer we de AccountService vragen een nieuwe account te registreren, deze de validatie component gebruikt om het email adres en de username te valideren. Dit zou nog verder uitgebreid kunnen worden om te controleren of er daadwerkelijk iets wordt weggeschreven, of het verwachte resultaat wordt teruggegeven, etc. Voor dit artikel houden we het echter hier bij.

Als deze test slaagt, weten we dus zeker dat AccountService::registerAccount() zowel Validator::email() en Validator::username() aanroept om de ingevoerde gegevens te controleren. Of die twee validatie methods daadwerkelijk iets nuttigs doen, maakt voor deze test niet uit.

In een andere test zouden we bijvoorbeeld kunnen simuleren dat Validator::username() vind dat de username ongeldig is. In dat geval moet de AccountService natuurlijk niet doorgaan met het registreren van de account, maar een error-status teruggeven. Dat zou er ongeveer als volgt uit kunnen zien:

/** 
 * Test that an invalid username causes an error condition. 
 * @covers AccountService::registerAccount 
 */ 
public function testRegisterAccount() 

    
//    Create mock validator 
    
$oValidatorMock $this->getMock('Validator', array('username')); 

    
//    Expect Validator->username('jgeerts') 
    
$oValidatorMock->expects($this->once()) 
        ->
method('username'
        ->
will($this->returnValue(false)); 

    
// Inject the validator into the account service. 
    
$this->object->setValidator($oValidatorMock); 

    
//    Create a dummy account. 
    
$oAccount = new Account(); 
    
$oAccount->setEmail('jory@E-sites.nl'); 
    
$oAccount->setUsername('jgeerts'); 

    
//    Do the actual test 
    
$oResult $this->object->registerAccount($oAccount);
    
$this->assertTrue($oResult instanceof AccountValidationError );
}

Hier controleren we dus of, wanneer de validator vind dat de username ongelding is, het resultaat van AccountService::registerAccount() een instantie is van de klasse AccountValidationError.

Met deze manier van testen zijn we in staat een erg stabiele codebase te hebben, waarbij we er zeker van kunnen zijn dat er niet 'zomaar ineens' ergens iets stuk gaat. Voor de website van “de bakker om de hoek” is het misschien een beetje over the top (met alle respect voor bakkers ;) ), maar als je 2~3 maanden full-time met 3 backend ontwikkelaars aan 1 project werkt, dan wil je wel zeker zijn dat de code die je in week 1 geschreven hebt aan het einde nog steeds werkt.

We proberen deze tests zoveel mogelijk te draaien voor we onze aanpassingen commiten naar onze code repository. Dit voorkomt dat er versies van een project zijn waar fouten in zitten. En omdat we allemaal ook maar menselijk en dus weleens iets vergeten, worden alle tests ook automatisch iedere nacht uitgevoerd op een centrale server. Mochten daarbij fouten gevonden worden dan krijgt het hele team daar een e-mail over, zodat de fout de volgende ochtend direct opgelost kan worden. Daarnaast moet diegene die de foutieve code gecommit heeft een euro betalen aan het schaap. We hopen binnenkort als team iets leuks met dit geld te doen, maar helaas (gelukkig?) eindigen er maar weinig euro's in het schaap.

[1]: We willen natuurlijk wel weten dat er een fout in het email validatie component zit, maar daar hebben we een losse test voor die verschillende (valide en invalide) email adressen laat valideren en controleerd of de validatie wel correct werkt.

Responsive design, hoe je website op meerdere apparaten hetzelfde doel behaalt

Door John van Hulsen

Weet jij de resolutie van het scherm waarop je dit verhaal momenteel leest? Misschien wel maar misschien heb je zelfs nog nooit van de term resolutie… - Lees meer

Lees verder