BDD (Behaviour-Driven Development) maakt het mogelijk acceptatiecriteria voor een stukje software te testen. In deze blog laten we zien hoe je met Spock code kunt testen volgens het BDD-principe.

De wortelfunctie getest in Spock

Laten we uitleggen wat BDD is aan de hand van een voorbeeld. We gaan de sqrt-functie testen. Dit is uiteraard niet nodig (we gaan ervan uit dat Oracle deze functie al goed getest heeft), maar ter illustratie hoe je een test met Spock opzet.

De sqrt-functie biedt de wortel van een gegeven getal. Hieronder zie je een testscenario voor de sqrt-functie.

acceptatiecriteria sqrt-functie
De sqrt-functie moet wortels kunnen opleveren voor een gegeven getal.
Scenario: De wortel van 9 moet 3 zijn
Wanneer: de invoer 9 is
Dan: zal het resultaat 3 moeten zijn

Als we voor dit scenario een test willen schrijven in Spock, ziet dit er als volgt uit

def "De wortel van 9 moet 3 zijn"() {
    given:"het getal"
    def getal = 9

    when:"de wortel van 9 wordt opgevraagd"
    def wortelVanNegen = Math.sqrt(getal)

    then:"de wortel van 9 moet 3 zijn"
    wortelVanNegen == 3
}

De wortel van 9 moet 3 zijn, zou natuurlijk ook zo een testscenario kunnen zijn voor een userstory.

Goed om te weten, Spock tests draaien in Groovy. Dit:

def "De wortel van 9 moet 3 zijn"()

is dus gewoon een method definitie.

Omdat we vrije namen kunnen gebruiken, kunnen we hier dus ook acceptatiecriteria als naam opvoeren. Verder zien we een given, when en then in de functie staan:

• given: is om aan te geven wat de uitgangssituatie is die je wil gaan testen. In ons voorbeeld een getal met de waarde 9.
• when: de aanroep die je wil testen. Nu de aanroep van Math.sqrt.
• then: definieert de verwachte situatie na uitvoer van het when-gedeelte. In dit geval controleren we of dat wortelVanNegen inderdaad 3 is.

Data driven testing

Wat nu als we de wortelfunctie willen testen met meerdere waardes? In onderstaand voorbeeld voeren we verschillende getallen in de functie in en controleren of het verwachte resultaat ook geleverd is door sqrt.

@Unroll
def "De wortel van #getal moet #expectedResult opleveren"() {
    when:"de wortel van 9 wordt opgevraagd"
    def result = Math.sqrt(getal)

    then:"de wortel van 9 moet 3 zijn"
    result == expectedResult

    where:
    getal|expectedResult
    16   | 4
    9    | 3

}

Draaien we deze test in IntelliJ, dan zien we ook netjes dat de testen zijn uitgevoerd met de verschillende waarden.

Spock
Hieronder zie je de complete test class.

package com.util

import spock.lang.Specification
import spock.lang.Unroll

class MathSpec extends Specification{
    def "De wortel van 9 moet 3 zijn"() {
        given:"het getal"
        def getal = 9

        when:"de wortel van 9 wordt opgevraagd"
        def wortelVanNegen = Math.sqrt(getal)

        then:"de wortel van 9 moet 3 zijn"
        wortelVanNegen == 3
    }

    @Unroll
    def "De wortel van #getal moet #expectedResult opleveren"() {
        when:"de wortel van 9 wordt opgevraagd"
        def result = Math.sqrt(getal)

        then:"de wortel van 9 moet 3 zijn"
        result == expectedResult

        where:
        getal|expectedResult
        16   | 4
        9    | 3

    }


}

Test-classes in Spock heten Specs dus is de naam niet TeTestenKlasseTest maar TeTestenKlasseSpec. Overigens ben je vrij om hier toch Test van te maken. Maar aangezien we met Specs testen, is het wel zo netjes onze tests dan ook Spec te noemen.

Hoe kunnen we Spock in een Maven-project gebruiken?

Voordat we verder gaan, doen weer eerst even een stapje terug en geven we antwoord op de vraag hoe we Spock kunnen opnemen in ons Maven-project. We nemen allereerst de volgende twee dependencies op (in de <dependencies> tag):

<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>
    <version>1.1-groovy-2.4</version>
    <scope>test</scope>
</dependency>
<dependency> 
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.4.13</version>
</dependency>
 

Vervolgens moeten we nog de volgende plugins toevoegen aan de <plugin> tag die weer in de <build> tag terug te vinden is.

<plugin>
    <!-- Nodig voor Groovy -->
    <groupId>org.codehaus.gmavenplus</groupId>
    <artifactId>gmavenplus-plugin</artifactId>
    <version>1.6</version>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
                <goal>compileTests</goal>
            </goals>
        </execution>
    </executions>
</plugin>
<!-- Test files noemen Spec's in Spock -->
<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.20.1</version>
    <configuration>
        <useFile>false</useFile>
        <includes>
            <include>**/*Spec.java</include> <!-- vraag me niet waarom maar die .java moet ook -->
            <include>**/*Test.java</include> <!-- junit testen ook mogelijk -->
        </includes>
    </configuration>
</plugin>

Even wat opmerkingen bij de plugins. Binnen BDD, noemen we de test-classes niet XxxTest imaar XxxSpec. We geven zo aan dat classes die eindigen op Spec ook meegenomen moeten worden tijdens het uitvoeren van de tests.

Zoals je in het commentaar ziet, vind ik het niet echt logisch dat er **/*Spec.java moet staan. Groovy-bestanden hebben namelijk de extensie .groovy en niet .java. Maar deze instelling is wel nodig.

Interaction based testing

In onderstaande voorbeeld wordt een KlantenController getest. De KlantenController maakt gebruik van een KlantenService. Als we de KlantenController gaan testen, willen we ons zuiver richten op de correcte werking van de KlantenController.

We weten niet welke implementatie gebruikt zal worden. Daarom willen we alleen maar controleren of de service aangeroepen wordt op de manier die we verwachten. Ook willen we natuurlijk zien dat de KlantenController het antwoord van de KlantenService verwerkt in het resultaat.

def "klantOpKlantNummer moet een klant op klantnummer kunnen ophalen"() {
    given:"klantenservice, klantenController"
    IKlantenService klantenService = Mock()
    KlantenController klantenController = new KlantenController(klantenService)
    def resultaatKlantKlantenService = Optional.of(new Klant())

    when:"klant wordt opgehaald op klantnummer 123"
    def klant = klantenController.getKlantOpKlantNummer("123")

    then:"klantenservice wordt aangeroepen voor klant met klantnummer 123"
    1 * klantenService.getKlantOpKlantNummer("123") >> resultaatKlantKlantenService

    and:"object van klantenservice is terug gegeven"
    klant== resultaatKlantKlantenService.get()
}

De KlantenService is een mock. In het then-stuk zie je dat verwacht wordt dat de methode KlantenService.getKlantOpKlantNummer 1 keer moet worden aangeroepen met de parameter “123” en dan resultaatKlantKlantenService als return value moet geven.

De KlantenService geeft een optional terug. We checken hier direct of de waarde klant inderdaad de door de service teruggegeven waarde van de optional is. Indien de klant niet gevonden is, dan zal de Optional geen waarde hebben. Eigenlijk willen we dit ook testen.

def "klantOpKlantNummer moet een exception opgooien indien klant niet bestaat"() {
    given:"klantenservice, klantenController"
    IKlantenService klantenService = Mock()
    KlantenController klantenController = new KlantenController(klantenService)

    when:"klant wordt opgehaald op klantnummer"
    def klant = klantenController.getKlantOpKlantNummer("123")

    then:"klantenservice geeft een empty terug"
    1 * klantenService.getKlantOpKlantNummer("123") >> Optional.empty()


    and:"de klantenController gooit een exception op"
    thrown(NietGevondenException)
}

Hierboven testen we het gewenste gedrag als een klant niet bestaat. Ook hier zien we weer de bekende given-, when-, then- en and-blokken. En wordt door de thrown(NietGevondenException) regel gecontroleerd of de controller inderdaad een exception heeft opgegooid.

Aantal aanroepen van een bepaalde methode

De regel 1 * klantenService.getKlantOpKlantNummer(“123”) >> resultaatKlantKlantenService begint met 1 * waarmee we aangeven dat een method 1 keer moet worden aangeroepen. We kunnen hier ook een range gebruiken.

Stel we verwachten dat een methode ergens tussen de 1 en 3 keer wordt aangeroepen, kunnen we dit ook opgeven. (1..3) * klantenService.getKlantOpKlantNummer(_) >> resultaatKlantKlantenService

We hebben het klantnummer nu vervangen door een underscore. We weten namelijk niet met welke parameters getKlantOpKlantNummer wordt aangeroepen.

Valideren parameters bij een methode aanroep

We kunnen de underscore vervangen door _ as String waarbij we aangeven dat we verwachten dat getKlantOpKlantNummer zal worden aangeroepen met een string parameter. Door een predicate te gebruiken kunnen we zelf checks verzinnen waar een parameter aan zal moeten voldoen.

Bijvoorbeeld als we verwachten dat getKlantOpKlantNummer aangeroepen wordt met een klantnummer wat met ‘123’ zal beginnen. (1..3) * klantenService.getKlantOpKlantNummer({it.startsWith(‘123’)}) >> resultaatKlantKlantenService`

Easy does it

Met Spock kunnen we een sterke koppeling maken tussen onze tests en de scenarios die we willen testen (BDD). Het is al met al een krachtig testing framework dat datadriven testing en interaction based testing supereenvoudig maakt.

Tot zover deze eerste kennnismaking. Ik wens je veel succes met Behaviour-Driven Development. En mocht je vastlopen en/of hulp kunnen gebruiken – aarzel niet en neem contact op!