Tool van de maand: Leesbare unit tests met SpecFlow/Cuke4Duke

 
11 oktober 2011

Soms kom je een tooltje tegen waar je echt helemaal blij van wordt. In mijn geval heet dat tooltje Cucumber. Cucumber is een nieuwe manier om tests mee te schrijven – en wel op een uiterst leesbare, begrijpelijke manier. Er bestaan verschillende smaken van, zoals Cuke4Duke voor de JVM, SpecFlow of Cuke4Nuke voor .Net, en varianten voor nog een aantal andere talen en frameworks. Cuke4Duke en SpecFlow zijn een sausje over andere testframeworks heen – maar het is wel een heel erg lekker sausje. Proef maar:

Feature: Course progress
  Course progress should be kept track of automatically
 
Scenario: someone starts a course
  Given a user with id 37
  Given a course translation with id 13
  When user 37 starts with course translation 13
  Then his current progress is at module 0

Het bovenstaande is een test voor een applicatie voor het online volgen van opleidingen. Het lijkt me duidelijk wat er getest wordt. Een ingewikkelder voorbeeld:

#language: nl-NL
Functionaliteit: Beschikbaarheid
	Het meten van beschikbaarheid
 
Abstract Scenario: De beschikbaarheid van een divisie
	Gegeven een machine met naam Machine1
	En een machine met naam Machine2
	En een divisie met naam Divisie1 en
	    machines Machine1, Machine2
	Als het volgende productieschema geldt:
		|Tijdstip	|Activiteit	|
		|0:00		|niets		|
		|9:00		|werken		|
		|17:00		|niets		|
	En de machine Machine1 de volgende statussen had:
		|Tijdstip	|Machinestatus	|
		|0:00		|uit		|
		|10:00		|productie	|
		|12:00		|uit		|
	En de machine Machine2 de volgende statussen had:
		|Tijdstip	|Machinestatus	|
		|0:00		|uit		|
		|14:00		|productie	|
		|16:00		|uit		|
	Dan is de beschikbaarheid tussen <start> en <eind>
	    van Divisie1 <beschikbaarheid> %
 
	Voorbeelden:
	|start	|eind	|beschikbaarheid|
	|0:00	|23:59	|25		|
	|9:00	|10:00	|0		|
	|9:00	|13:00	|25		|
	|14:00	|16:00	|50		|
	|10:00	|15:00	|30		|

Ik hoef er waarschijnlijk niet bij te vertellen dat dit voorbeeld komt uit een applicatie dat voor het management van een fabriek bijhoudt hoeveel van de geplande tijd de machines hebben aangestaan: dat was uit de beschrijving van de test volgens mij al duidelijk. Ondanks dat het bovenstaande in het Nederlands is geschreven, is het letterlijk (een deel van) een testscript dat we voor deze applicatie gebruiken. We hadden ook Engels kunnen gebruiken, of Duits, Frans, Noors, Chinees, Esperanto, of een van de nog een stuk of 40 andere talen die worden ondersteund.

Omdat het hier een Abstract Scenario betreft, zijn dit in feite zelfs vijf unit tests ineen – namelijk de vijf Voorbeelden. De voorbeelden worden een voor een gematcht met de placeholders in de scenariobeschrijving, en als input voor de test gebruikt. Dit maakt het mogelijk om op een hele compacte en intuitief duidelijke manier meerdere tests te specificeren zonder dat het onleesbaar wordt, en het is erg simpel om nieuwe gevallen toe te voegen.

Cucumber is opgezet vanuit het BDD-gedachtegoed (Behaviour Driven Design), waarbij elk scenario of abstract scenario is opgebouwd door een drietal stappen die overeenkomen met het standaard BDD-patroon Arrange-Act-Assert.

  • De eerste stap (arrange) wordt gekenmerkt door het keyword Gegeven of Stel (in het Nederlands althans); hiermee beschrijf je de state zoals die voor de te testen functionaliteit wordt verondersteld.
  • De tweede stap (act) is de te testen actie, en wordt aangegeven met het keyword Als.
  • Tenslotte is er de stap Dan (assert) die test of de verwachte uitkomst ook plaatsgevonden heeft.

Als je meerdere Gegeven-, Als– of Dan-stappen wilt specificeren dan kan je ook (zoals hierboven) En of zelfs Maar gebruiken – dat betekent hetzelfde maar leest een stuk lekkerder. Je kan deze keywords (of hun engelse equivalenten Given, When, Then, And, But) makkelijk terugvinden in de voorbeelden hierboven.

De tests die je schrijft zijn daarmee zoals je ziet volledig leesbaar en begrijpbaar, zeker als je weet waar de applicatie over gaat, omdat ze in de taal van de business geschreven zijn. Je kunt hiermee zonder moeite en uitleg direct bij de klant aankomen voor terugkoppeling, uitbreiding, of vragen. (“Is dit inderdaad de bedoeling?”, “Hoe moet de applicatie in dit geval reageren?”, “Kloppen deze rekenvoorbeelden?”)

De scenario’s die je schrijft zet je in een bestand met de extensie .feature, en deze worden dan door SpecFlow of Cuke4Duke automatisch omgezet in een unit test voor consumptie door JUnit, NUnit, MSTest, of nog andere unit test frameworks. Natuurlijk gaat dat niet vanzelf – je moet nog wel iets aan achterliggende code schrijven.

Dat noemen ze dan de step definitions file, en is gewoon een Java- of C#-klasse waar annotaties (resp. attributen) in staan die overeenkomen met de bovengenoemde drie stappen. Elke stap komt overeen met een methode die wordt gematcht aan de hand van de annotaties, en het doorlopen van een stap is niet meer dan het uitvoeren van de overeenkomstige methode:

  • de code die wordt uitgevoerd bij de Gegeven-stappen is die welke geannoteerd is met het attribuut [Given("...")] (dan wel de annotatie @Given(...)), waarin je de beoogde beginstate opzet.
  • Voor de Als-stap annoteer je een methode met @When("..."), en voer je de te testen actie uit.
  • In de Dan-stap wordt de methode met annotatie @Then("...") uitgevoerd – hierin zet je je assertEqual(expected, actual)-statements e.d. in.

Een voorbeeld:

namespace MyApp.Test
{
    [Binding]
    [StepScope(Feature = "Beschikbaarheid")]
    public class AvailabilitySteps
    {
	[Given("een machine met naam (.*)")]
	[Given("a machine named (.*)")]
	public void GivenAMachine(String name)
	{
	    // maak een machine aan en sla die op in een field in deze klasse
	}
    }
 
    // ...meer step definitions e.d...
}

Zoals je bij bovenstaande methode (GivenAMachine(String name)) ziet krijgt elk attribuut steeds een regex mee dat in het feature-bestand gematcht wordt. Een regex capture wordt vervolgens als argument meegegeven aan de geannoteerde methode, waarbij eventueel ook nog type-conversie plaatsvindt:

@Then("his current progress is at module (.*)")
public void ThenHisCurrentProgressIsAtModule(int modulenr) {
    assertEquals(modulenr, uc.getReachedProgress());
}

Zoals je ziet kan je ook in de scenario-omschrijving zelf tabellen opnemen. Anders dan bij de Voorbeelden is er in dit geval geen automatische invulling en conversie met behulp van placeholders (want hoe zou dat überhaupt moeten werken?). In plaats daarvan krijg je een object binnen van type Table die je op verschillende manieren kan uitlezen, waarvan de makkelijkste waarschijnlijk is dat je de methode tabel.hashes() (met hoofdletter in SpecFlow) aanroept. Dat levert dan een List<Map<String,String>> (oftewel een List<Dictionary<string,string>>) op. Die lijst heeft één entry per tabelrij, en de map/dictionary geeft aan welke kolom (key) welke waarde (value) bevat – bijvoorbeeld: hierboven geldt

productieschematabel.Hashes()[1]["Activiteit"] == "werken"

Deze waarden kan je dan vervolgens zelf parsen naar het datatype (zoals bijv. een DateTime) dat je nodig hebt.

Cucumber kent nog een aantal andere handigheidjes:

  • Je kunt meerdere step definition files maken zodat gerelateerde functionaliteit bijelkaar blijft, en mbv. dependency injection de state van deze files met elkaar delen.
  • Je kan in je feature-bestand een gezamenlijke Achtergrond-beginstate specificeren die dan voor elk scenario in dat bestand geldt.
  • Je kan setup- en teardown-code specificeren met een @Before– en @After-annotaties; SpecFlow kent ook nog een [BeforeFeature]– en [AfterFeature]-attribuut die je op een statische methode kan zetten.
  • Je kan features en scenario’s taggen zodat je bepaalde tests kan laten negeren, of alleen maar in zwaardere testrondes (integratietests, nightly builds, …) uitvoeren, of bepaalde steps alleen voor bepaalde tags laten gelden.
  • enz. enz.

Kortom: Cucumber is een leuke, flexibele tool, die zich ook nog eens makkelijk laat installeren. Alle reden om er ook eens mee te stoeien!


Werken met ?
Kijk dan bij onze mogelijkheden voor zowel starters als ervaren engineers.


Categorieën: Development

Tags: , ,