Ontwikkelaars maken al jaren gebruik van unit testen, maar meestal nadat de programmacode is ontworpen en geschreven. Je kunt je voorstellen dat het schrijven van een test achteraf niet eenvoudig is. Het wordt dan ook vaak achterwege gelaten. Het testen van de code gebeurt dan meer tijdens gebruikerstesten.
Test-driven development (TDD) probeert dit probleem op te lossen en zorgt er voor dat de geschreven programmacode van een hogere kwaliteit is. De testen worden in dit geval geschreven vóór de code. Dit artikel, het eerste deel van een serie van twee, beschrijft hoe je je programmacode zo kunt schrijven dat het eenvoudiger is om er (automatische) unit tests mee uit te voeren.
Wat zijn unit testen?
Een unit test wordt vaak omschreven als een programma dat bedoeld is om een batch te draaien om daarin classes te testen. Een dergelijk programma stuurt de class een vast bericht en gaat na of het verwachte antwoord terugkomt. In de praktijk komt het er op neer, dat je een programma schrijft dat de publieke interfaces van de classes in je applicatie test. Let er op dat dit dus niet een functionaliteitstest of acceptatietest is. Het gaat er puur om dat de methoden in je class dat doen wat je ervan verwacht.
Je schrijft een programma dat de publieke interfaces van de classes test
Het kan een behoorlijke taak zijn om dit goed te doen. Je moet eerst bepalen welke tools je gebruikt om je tests te creëren. In het verleden zijn er grote testsuites beschikbaar gekomen met ingewikkelde scripts die ideaal waren voor gespecialiseerde Quality Assurance (QA) teams. Deze suites zijn echter minder geschikt voor unit testen. Het ligt voor de gemiddelde programmeur veel meer voor de hand om testen te ontwikkelen die gebruik maken van de zelfde taal en IDE als waar ze de applicatie in ontwikkelen.
Veel moderne unit test frameworks zijn gebaseerd op het framework van Kent Beck voor het eerste XP project, genaamd Chrysler C3. Dit framework is geschreven in Smalltalk en bestaat nog steeds, alhoewel er in de loop van de tijd meerdere revisies zijn geweest. Later is dit framework geport naar Java en werd toen JUnit genoemd. Daarna zijn er implementaties gekomen in C++, VB, Python, Perl, etc.
Het NUnit Test Framework
NUnit is een unit-test framework voor alle .NET talen. Versie 2.0 is volledig nieuw ontworpen om gebruik te maken van de mogelijkheden van het .NET Framework. De broncode in C# is voor iedereen beschikbaar, omdat het een open source project is. NUnit 2.0 is anders dan haar voorgangers. Deze voorgangers maakten gebruik van base classes voor de hiervan afgeleide test classes. Er was geen andere manier om het voor elkaar te krijgen. Helaas betekende dat ook dat het restricties opwierp voor het bouwen van de testcode, omdat veel talen (zoals Java en C#) alleen single inheritance ondersteunen. Het opbouwen van de testcode was daardoor lastig en leidde al snel tot complexe class-hiërarchieën.
De broncode in C# is voor iedereen beschikbaar, omdat het een open source project is
Het .NET Framework introduceerde een nieuw programmeerconcept waarmee dit probleem kon worden opgelost: attributen. Met attributen ben je in staat metadata aan de code toe te voegen. Meestal hebben deze attributen geen invloed op de wijze waarop de code zelf functioneert, maar het biedt extra informatie over de geschreven programmacode. Vaak gebruik je attributen om je code te beschrijven, maar het kan ook gebruikt worden om informatie over de assembly te geven aan een programma dat deze assembly nooit eerder gezien of gebruikt heeft.
Dat is precies de manier waarop NUnit werkt. De Test Runner applicatie doorzoekt je gecompileerde code naar attributen die vertellen welke classes en methoden testclasses zijn. Vervolgens wordt reflection gebruikt om deze methoden uit te voeren. Je hoeft enkel gebruik te maken van de juiste attributen.
NUnit levert een reeks van attributen die je kunt gebruiken voor het maken van een unit test. Met deze attributen definieer je test fixtures, test methoden, setup en teardown methoden. Er zijn ook attributen voor het aangeven van de verwachtte exceptions of om een bepaalde test over te slaan.
TestFixture Attribuut
Het TestFixture attribuut is bedoeld om aan te geven dat een class test methoden bevat. Wanneer je dit attribuut toevoegt aan een class in je project, zal de Test Runner deze class scannen voor test methoden.
Het TestFixture attribuut is bedoeld om aan te geven dat een class test methoden bevat
In de volgende paragrafen laten we de werking van NUnit zien. Om de voorbeeld zelf te kunnen proberen moet je natuurlijk beschikken over NUnit. Versie 2.1 (op dit moment de meest recente) is te downloaden via www.nunit.org. Omdat NUnit een open source project is, kun je de tools gratis gebruiken.
In het volgende voorbeeld zie je hoe je dit attribuut gebruikt. De gebruikte code is VB.NET, maar C# is natuurlijk ook mogelijk. Na het installeren van NUnit kun je in je .NET project referenties toevoegen voor de NUnit assemblies.
Imports System
Imports NUnit.Framework
<TestFixture()> _
Public Class UnitTests
End Class
De enige voorwaarde voor classes die het TestFixture attribuut gebruiken, is dat ze een publieke default constructor moeten hebben (of geen constructor, dat is het zelfde).
Test Attribuut
Het Test attribuut wordt gebruikt om aan te geven dat een methode in een test fixture gestart moet worden door de Test Runner applicatie. De methode moet publiek zijn en niets terug geven (een Sub in VB.NET, function void in C#). Ook mag de methode geen parameters hebben, anders wordt de methode niet getoond in de Test Runner GUI en zal ook niet aangeroepen worden bij het starten van de TestFixture.
De volgende code illustreert het gebruik van het Test attribuut.
Imports System
Imports NUnit.Framework
<TestFixture()> _
Public Class UnitTests
<Test()> _
Public Sub TestA()
'doe iets
End Sub
End Class
Je kunt in de constructor van het Test attribuut overigens ook een omschrijving meegeven, net als bij het TestFixture attribuut.
SetUp en TearDown attributen
Het komt voor dat je bij het samenstellen van de Unit Tests eerst bepaalde handelingen moet uitvoeren voor en na het draaien van een test. Je kunt een private methode creëren en die aanroepen vanuit iedere testmethode, maar je kunt ook Setup en TearDown attributen gebruiken. Deze attributen geven aan dat een methode moet worden gestart vóór en ná ieder testmethode in de Test Fixture. Het meest voor de handliggende gebruik van deze attributen is bij het creëren van objecten waar de testmethode van afhankelijk is (database connecties bijvoorbeeld).
In het volgende voorbeeld zie je hoe je SetUp en TearDown gebruikt.
Imports System
Imports NUnit.Framework
<TestFixture()> _
Public Class UnitTests
Private _DbConn As String
<SetUp()> _
Public Sub SetupTest()
_DbConn = "server=(local);password=;user id=sa;"
End Sub
<TearDown()> _
Public Sub TeardownTest()
_DbConn = ""
End Sub
<Test()> _
Public Sub TestA()
'doe iets
End Sub
End Class
ExpectedException attribuut
Het komt ook voor dat je een situatie wilt creëren waar zeker een exception optreedt. Dat is het meest eenvoudig te realiseren via het ExpectedException attribuut, zoals in het volgende voorbeeld.
Imports System
Imports NUnit.Framework
<TestFixture()> _
Public Class UnitTests
<Test(), _
ExpectedException(GetType(InvalidCastException))>_
Public Sub TestA()
'doe iets
End Sub
End Class
Zodra deze code wordt uitgevoerd,zal de test alleen slagen wanneer er een InvalidCastException wordt opgeworpen. Je kunt, zoals je ziet, meerdere attributen achter elkaar plaatsen (onder elkaar in C#) om meerdere exception-types op te geven. Probeer dat echter zo min mogelijk te doen. Een test is er veelal voor bedoeld om één ding te testen. Houdt er ook rekening mee dat het attribuut niet op geërfde typen controleert. Als er een exception optreedt die afgeleid van een InvalidCastException, dan faalt de test toch.
Een test is er veelal voor bedoeld om één ding te testen
Ignore attribuut
Je zult dit attribuut waarschijnlijk niet zo veel gebruiken, maar als je het nodig hebt, is het toch handig. Als je wilt dat een bepaalde test niet moet lopen, dan gebruik je het Ignore attribuut, zoals in onderstaand voorbeeld.
_
Public Sub TestToIgnore()
End Sub
Het is beter om dit attribuut te gebruiken in plaats van het ‘uit-commentariëren’ van de methode. Op deze manier blijft de test nog steeds beschikbaar, en wordt je er in de Test Runner output toch aan herinnerd.
NUnit Assertion Class
Naast de attributen die je kunt gebruiken om testen in je code te identificeren, bevat NUnit nog een belangrijke class. Dit is de Assertion class die je statische methoden geeft die je kunt gebruiken in je testmethoden om na te gaan dat hetgeen is gebeurd ook het gewenste gedrag is geweest. Een voorbeeld maakt het wellicht wat duidelijker.
_
Public Sub TestB()
Dim user As String = "piet puk"
Assertion.AssertEquals("piet puk", user)
End Sub
Het is niet het mooiste staaltje code, maar je begrijpt waarschijnlijk wel wat de bedoeling is. Op ieder moment in de testcode kan je met Assertion-methode bekijken wat de state is van een variabele.
Tests uitvoeren
Nu dat we de beginselen kennen van de testcode, moeten we nu weten hoe we de testen kunnen uitvoeren. Dat is eigenlijk vrij eenvoudig. NUnit beschikt over twee Test Runner applicaties. een Windows applicatie en een Console versie. Beide zijn in staat om XML output te generen. Vooral de consoleversie is handig wanneer je nachtelijke testbatches wilt draaien op de geschreven programmacode.
Om de Windows versie te gebruiken start je de applicatie (na installatie terug te vinden via Start – All Programs). In het programma open je de assembly (dll of exe) die je Test Fixtures bevat. Je krijgt een boomstructuur te zien van de classes en methodes in de assembly. Je kunt individuele methoden en classes testen, maar ook alle tests door op Run te klikken.

Er zijn situaties, bijvoorbeeld wanneer je geautomatiseerde buildscripts hebt gemaakt (via NAnt), waarbij testen via een GUI niet praktisch is. Hiervoor is de NUnit console applicatie meer geschikt. Deze genereert een XML bestand die je met XSLT kunt transformeren naar HTML of een ander formaat zodat de testresultaten getoond en afgedrukt kunnen worden. De NUnit documentatie geeft meer uitleg over het gebruik van de console applicatie.
NUnit beschikt over twee Test Runner applicaties. een Windows applicatie en een Console versie
Test-Driven Development
Oké, nu weet je hoe je unit tests moet schrijven, toch? Helaas is testen net als programmeren. Alleen de syntax kennen is niet voldoende. Kennis en vooral kunde van testmethoden en -technieken zijn essentieel om betrouwbare software te schrijven. Test-Driven Development (TDD) maakt daar een onderdeel van uit. Als je niet bekend bent met TDD komt het je misschien op het eerste gezicht vreemd over.
Er wordt geen regel code geschreven tot je een test hebt die mislukt
De traditionele manier van software ontwikkeling begint met het ontwerp van je classes, het schrijven van de implementatie en vervolgens het testen. Wanneer testen je uitgangspunt is, is de aanpak anders. In plaats van het ontwerpen van een component, het schrijven van de programmacode en daarna testen, draaien we de zaak namelijk om. Er wordt geen regel code geschreven tot je een test hebt die mislukt. In dit geval gaat het schrijven van je programma als volgt:
1. schrijf een test
2. draai de test; de compilatie zal mislukken, want de code die je test aanspreekt zal niet bestaan (derhalve mislukt ook de test).
3. schrijf de kale structuur van je class om te testcode te kunnen compileren.
4. draai de test; de test zal mislukken (als dat niet zo is, is de test niet goed).
5. implementeer de code om de test succesvol te doorstaan.
6. draai de test; nu slaagt de test (ga anders terug naar stap 5).
7. ga verder met stap 1.
Als je bezig bent met stap 5, schrijf je code volgens een proces dat ‘Coding by Intention’ heet. Je programmeert dan top-down in plaats van bottom-up. Je denkt niet “ik heb deze class met deze methodes nodig”, maar je schrijft code voordat de class die je nodig hebt bestaat. Het compileren van de code zal dan mislukken, want de compiler kan de class niet vinden. Op zich is dat goed, want een mislukte compilatie geldt als een mislukte test.
Wanneer je op deze manier programmeert, beschrijf je de bedoeling van de code die je nodig hebt. Dit zorgt niet alleen voor goed geteste code, maar het is vaak leesbaarder en beter te debuggen. Ook het ontwerp verbetert. Bij traditionele softwareontwikkeling wordt getest of een bestaand stuk code correct geschreven was. Bij TDD worden testen gebruikt om het gedrag van een class te definiëren voordat je hem schrijft. Dat is niet per sé makkelijker, maar wel netter.
Als je bekend bent met Extreme Programming, dan kun je dit zien als een review. Een voorbeeld zal het e.e.a. verduidelijken. Stel dat we een applicatie hebben die in staat is om geld op te nemen van en te storten op een bankrekening. Voordat we een BankRekening class maken, schrijven we eerst een test. Deze test ziet er als volgt uit:
:="Storten op rekening")> _
Public Sub BankRekeningTest()
Dim br As New BankRekening
br.Stort(25.0)
br.Stort(100.0)
Assertion.AssertEquals(125.0, br.Saldo)
End Sub
Deze code zal niet compileren, want de BankRekening class bestaat niet. Dit is typisch een vorm van Test-Driven Development. Schrijf geen code tot je een test hebt die mislukt. Het niet kunnen compileren geldt ook als een mislukte test.
Dan schrijven we een BankRekening class die in ieder geval code bevat om de test te laten compileren.
Public Class BankRekening
Public Saldo As Double
Public Sub Stort(ByVal bedrag As Double)
End Sub
End Class
De code compileert nu prima, dus we kunnen de test starten. De test mislukt met de volgende reden:
UnitTests.UnitTests.BankRekeningTest:
expected:<125>
but was:<0>
We moeten dus de class uitbreiden om er voor te zorgen dat de test wel goed verloopt.
Public Class BankRekening
Private _saldo As Double
Public Sub Stort(ByVal bedrag As Double)
_saldo += bedrag
End Sub
Public ReadOnly Property Saldo() As Double
Get
Return _saldo
End Get
End Property
End Class
Deze keer gaat de test wel goed.
Conclusies
Serieuze ontwikkelaars schrijven betrouwbare programmacode. Unit testen zijn een belangrijk instrument om te bepalen of de code inderdaad doet wat ervan wordt verwacht. Test-Driven Development is een methode die, gebruikmakend van unit testen, een ontwikkelmethode voorstelt waarmee eerste de test en dan pas de code wordt geschreven. Op deze manier is de programmacode leesbaarder, beter te debuggen en is ook het ontwerp potentieel beter.
De open-source tool NUnit maakt het de ontwikkelaar makkelijk om TDD als methodiek te hanteren, en op zijn minst het unit testen van zijn componenten te automatiseren.
In het volgende artikel over unit testen gaan we in op het gebruik van nepobjecten en het testen van de verschillende lagen (data, business en presentatie) in een applicatie.
Meer informatie
http://www.nunit.org/default.htm
http://www.msdnaa.net/Resources/display.aspx?ResID=2365
http://www.devcity.net/net/article.aspx?alias=xprogramming
http://www.tangent-studios.com/programming/csharp/NUnit2Tut/NUnitV2Tut.htm