Einführung in das Testen mit Spock und Groovy

Einführung in das Testen mit Spock und Groovy

1. Einführung

In diesem Artikel werfen wir einen Blick aufSpock, das Testframework vonGroovy. Hauptsächlich möchte Spock eine leistungsfähigere Alternative zum traditionellen JUnit-Stack sein, indem Groovy-Funktionen genutzt werden.

Groovy ist eine JVM-basierte Sprache, die sich nahtlos in Java integriert. Neben der Interoperabilität bietet es zusätzliche Sprachkonzepte wie dynamische Funktionen, optionale Typen und Metaprogrammierung.

Durch die Verwendung von Groovy führt Spock neue und ausdrucksstarke Methoden zum Testen unserer Java-Anwendungen ein, die mit normalem Java-Code einfach nicht möglich sind. In diesem Artikel werden einige der wichtigsten Konzepte von Spock anhand einiger praktischer Schritt-für-Schritt-Beispiele erläutert.

2. Maven-Abhängigkeit

Bevor wir beginnen, fügen wir unsereMaven dependencies hinzu:


    org.spockframework
    spock-core
    1.0-groovy-2.4
    test


    org.codehaus.groovy
    groovy-all
    2.4.7
    test

Wir haben sowohl Spock als auch Groovy wie jede Standardbibliothek hinzugefügt. Da Groovy jedoch eine neue JVM-Sprache ist, müssen wir das Plugingmavenpluseinbinden, um es kompilieren und ausführen zu können:


    org.codehaus.gmavenplus
    gmavenplus-plugin
    1.5
    
        
            
                compile
                testCompile
            
        
     

Jetzt können wir unseren ersten Spock-Test schreiben, der in Groovy-Code geschrieben wird. Beachten Sie, dass Groovy und Spock nur zu Testzwecken verwendet werden. Aus diesem Grund sind diese Abhängigkeiten testspezifisch.

3. Struktur eines Spock-Tests

3.1. Technische Daten und Merkmale

Während wir unsere Tests in Groovy schreiben, müssen wir sie dem Verzeichnissrc/test/groovyanstelle vonsrc/test/java.hinzufügen. Erstellen wir unseren ersten Test in diesem Verzeichnis und nennen ihnSpecification.groovy:

class FirstSpecification extends Specification {

}

Beachten Sie, dass wir dieSpecification-Schnittstelle erweitern. Jede Spock-Klasse muss dies erweitern, um das Framework zur Verfügung zu stellen. Auf diese Weise können wir unsere erstenfeature:implementieren

def "one plus one should equal two"() {
  expect:
  1 + 1 == 2
}

Bevor wir den Code erklären, ist es auch erwähnenswert, dass in Spock das, was wir alsfeature bezeichnen, etwas synonym mit dem ist, was wir in JUnit alstest sehen. Alsowhenever we refer to a feature we are actually referring to a test.

Lassen Sie uns nun unserefeature analysieren. Auf diese Weise sollten wir sofort einige Unterschiede zu Java feststellen können.

Der erste Unterschied besteht darin, dass der Name der Feature-Methode als gewöhnliche Zeichenfolge geschrieben wird. In JUnit hätten wir einen Methodennamen, bei dem die Wörter in Kamelbuchstaben oder Unterstrichen getrennt werden, was nicht so aussagekräftig oder für den Menschen lesbar gewesen wäre.

Der nächste ist, dass unser Testcode in einemexpect-Block lebt. Wir werden in Kürze detaillierter auf Blöcke eingehen, aber im Wesentlichen sind sie eine logische Methode, um die verschiedenen Schritte unserer Tests aufzuteilen.

Schließlich stellen wir fest, dass es keine Behauptungen gibt. Dies liegt daran, dass die Behauptung implizit ist, bestanden wird, wenn unsere Aussage gleichtrue ist, und fehlgeschlagen, wenn sie gleichfalse ist. Auch hier werden wir die Behauptungen in Kürze ausführlicher behandeln.

3.2. Blöcke

Wenn wir JUnit einen Test schreiben, stellen wir manchmal fest, dass es keine aussagekräftige Möglichkeit gibt, ihn in Teile aufzuteilen. Wenn wir beispielsweise eine verhaltensgesteuerte Entwicklung verfolgen, können wir die Teile vongiven when thenmit Kommentaren bezeichnen:

@Test
public void givenTwoAndTwo_whenAdding_thenResultIsFour() {
   // Given
   int first = 2;
   int second = 4;

   // When
   int result = 2 + 2;

   // Then
   assertTrue(result == 4)
}

Spock behebt dieses Problem mit Blöcken. Blocks are a Spock native way of breaking up the phases of our test using labels. Sie geben uns Bezeichnungen fürgiven when then und mehr:

  1. Setup (Alias ​​durch Given) - Hier führen wir alle erforderlichen Einstellungen durch, bevor ein Test ausgeführt wird. Dies ist ein impliziter Block, bei dem Code, der sich in keinem Block befindet, Teil desselben wird

  2. When - Hier geben wirstimulus für das, was getestet wird. Mit anderen Worten, wo wir unsere zu testende Methode aufrufen

  3. Then - Hier gehören die Aussagen hin. In Spock werden diese als einfache boolesche Behauptungen ausgewertet, auf die später noch eingegangen wird

  4. Expect - Dies ist eine Methode, um unserestimulus undassertion innerhalb desselben Blocks auszuführen. Je nachdem, was wir aussagekräftiger finden, können wir diesen Block verwenden oder auch nicht

  5. Cleanup - Hier werden alle Testabhängigkeitsressourcen abgebaut, die sonst zurückbleiben würden. Beispielsweise möchten wir möglicherweise alle Dateien aus dem Dateisystem entfernen oder Testdaten entfernen, die in eine Datenbank geschrieben wurden

Versuchen wir erneut, unseren Test zu implementieren, wobei wir diesmal die Blöcke voll ausnutzen:

def "two plus two should equal four"() {
    given:
        int left = 2
        int right = 2

    when:
        int result = left + right

    then:
        result == 4
}

Wie wir sehen können, tragen Blöcke dazu bei, dass unser Test besser lesbar wird.

3.3. Groovy-Funktionen für Behauptungen nutzen

Within the then and expect blocks, assertions are implicit.

Meistens wird jede Anweisung ausgewertet und schlägt dann fehl, wenn sie nichttrue ist. Wenn Sie dies mit verschiedenen Groovy-Funktionen kombinieren, können Sie auf die Verwendung einer Assertion-Bibliothek verzichten. Versuchen wir die Behauptung vonlist, um dies zu demonstrieren:

def "Should be able to remove from list"() {
    given:
        def list = [1, 2, 3, 4]

    when:
        list.remove(0)

    then:
        list == [2, 3, 4]
}

Während wir in diesem Artikel nur kurz auf Groovy eingehen, lohnt es sich zu erklären, was hier passiert.

Erstens bietet Groovy einfachere Möglichkeiten zum Erstellen von Listen. Wir können unsere Elemente nur in eckigen Klammern deklarieren, und intern werdenlist instanziiert.

Zweitens können wir, da Groovy dynamisch ist,def verwenden, was nur bedeutet, dass wir keinen Typ für unsere Variablen deklarieren.

Im Zusammenhang mit der Vereinfachung unseres Tests ist schließlich die Überlastung des Bedieners die nützlichste Funktion. Dies bedeutet, dass intern anstelle eines Referenzvergleichs wie in Java die Methodeequals()aufgerufen wird, um die beiden Listen zu vergleichen.

Es lohnt sich auch zu demonstrieren, was passiert, wenn unser Test fehlschlägt. Lassen Sie es brechen und sehen Sie sich dann an, was auf der Konsole ausgegeben wird:

Condition not satisfied:

list == [1, 3, 4]
|    |
|    false
[2, 3, 4]
 

at FirstSpecification.Should be able to remove from list(FirstSpecification.groovy:30)

Während nurequals() auf zwei Listen aufgerufen wird, ist Spock intelligent genug, um eine Aufschlüsselung der fehlgeschlagenen Behauptung durchzuführen und uns nützliche Informationen für das Debuggen zu geben.

3.4. Ausnahmen geltend machen

Spock bietet uns auch eine aussagekräftige Möglichkeit, nach Ausnahmen zu suchen. In JUnit verwenden einige unserer Optionen möglicherweise einentry-catch-Block, deklarierenexpected oben in unserem Test oder verwenden eine Bibliothek eines Drittanbieters. Spocks native Behauptungen beinhalten eine Möglichkeit, mit Ausnahmen von der Stange umzugehen:

def "Should get an index out of bounds when removing a non-existent item"() {
    given:
        def list = [1, 2, 3, 4]

    when:
        list.remove(20)

    then:
        thrown(IndexOutOfBoundsException)
        list.size() == 4
}

Hier mussten wir keine zusätzliche Bibliothek einführen. Ein weiterer Vorteil besteht darin, dass die Methodethrown()den Typ der Ausnahme bestätigt, die Ausführung des Tests jedoch nicht anhält.

4. Datengesteuertes Testen

4.1. Was ist ein datengesteuertes Testen?

Im Wesentlichendata driven testing is when we test the same behavior multiple times with different parameters and assertions. Ein klassisches Beispiel hierfür wäre das Testen einer mathematischen Operation wie das Quadrieren einer Zahl. Abhängig von den verschiedenen Permutationen der Operanden ist das Ergebnis unterschiedlich. In Java ist der Begriff, mit dem wir vielleicht besser vertraut sind, das parametrisierte Testen.

4.2. Implementieren eines parametrisierten Tests in Java

In einigen Fällen lohnt es sich, einen parametrisierten Test mit JUnit durchzuführen:

@RunWith(Parameterized.class)
public class FibonacciTest {
    @Parameters
    public static Collection data() {
        return Arrays.asList(new Object[][] {
          { 1, 1 }, { 2, 4 }, { 3, 9 }
        });
    }

    private int input;

    private int expected;

    public FibonacciTest (int input, int expected) {
        this.input = input;
        this.expected = expected;
    }

    @Test
    public void test() {
        assertEquals(fExpected, Math.pow(3, 2));
    }
}

Wie wir sehen können, gibt es ziemlich viel Ausführlichkeit und der Code ist nicht sehr gut lesbar. Wir mussten ein zweidimensionales Objektarray erstellen, das sich außerhalb des Tests befindet, und sogar ein Wrapper-Objekt zum Einfügen der verschiedenen Testwerte.

4.3. Verwenden von Datentabellen in Spock

Ein einfacher Gewinn für Spock im Vergleich zu JUnit ist die saubere Implementierung parametrisierter Tests. In Spock ist dies wiederum alsData Driven Testing. bekannt. Lassen Sie uns nun denselben Test erneut implementieren. Nur dieses Mal verwenden wir Spock mitData Tables, was eine weitaus bequemere Möglichkeit bietet, einen parametrisierten Test durchzuführen ::

def "numbers to the power of two"(int a, int b, int c) {
  expect:
      Math.pow(a, b) == c

  where:
      a | b | c
      1 | 2 | 1
      2 | 2 | 4
      3 | 2 | 9
  }

Wie wir sehen können, haben wir nur eine einfache und aussagekräftige Datentabelle, die alle unsere Parameter enthält.

Neben dem Test gehört es auch dort hin, wo es sein sollte, und es gibt kein Boilerplate. Der Test ist ausdrucksstark, mit einem für Menschen lesbaren Namen und einem reinen Blockexpect undwhere, um die logischen Abschnitte aufzubrechen.

4.4. Wenn eine Datenbank fehlschlägt

Es ist auch sehenswert, was passiert, wenn unser Test fehlschlägt:

Condition not satisfied:

Math.pow(a, b) == c
     |   |  |  |  |
     4.0 2  2  |  1
               false

Expected :1

Actual   :4.0

Auch hier gibt uns Spock eine sehr informative Fehlermeldung. Wir können genau sehen, welche Zeile unserer Datentabelle einen Fehler verursacht hat und warum.

5. Verspottung

5.1. Was ist Spott?

Das Verspotten ist eine Möglichkeit, das Verhalten einer Klasse zu ändern, mit der unser zu testender Dienst zusammenarbeitet. Dies ist eine hilfreiche Methode, um die Geschäftslogik isoliert von ihren Abhängigkeiten zu testen.

Ein klassisches Beispiel hierfür wäre das Ersetzen einer Klasse, die einen Netzwerkaufruf durch etwas tätigt, das einfach so tut als ob. Für eine ausführlichere Erklärung lohnt es sich,this article zu lesen.

5.2. Verspotten mit Spock

Spock hat ein eigenes Verspottungs-Framework, das interessante Konzepte nutzt, die Groovy in die JVM gebracht hat. Lassen Sie uns zunächst einMock: instanziieren

PaymentGateway paymentGateway = Mock()

In diesem Fall wird der Typ unseres Mocks durch den Variablentyp abgeleitet. Da Groovy eine dynamische Sprache ist, können wir auch ein Typargument angeben, damit wir unseren Mock keinem bestimmten Typ zuordnen müssen:

def paymentGateway = Mock(PaymentGateway)

Wenn wir nun eine Methode fürPaymentGateway mock, aufrufen, wird eine Standardantwort gegeben, ohne dass eine echte Instanz aufgerufen wird:

when:
    def result = paymentGateway.makePayment(12.99)

then:
    result == false

Der Begriff hierfür lautetlenient mocking. Dies bedeutet, dass nicht definierte Scheinmethoden sinnvolle Standardwerte zurückgeben, anstatt eine Ausnahme auszulösen. Dies ist in Spock beabsichtigt, um Verspottungen und damit Sprödigkeit zu vermeiden.

5.3. Stubbing-Methode RuftMocks auf

Wir können auch Methoden konfigurieren, die für unseren Mock aufgerufen werden, um auf bestimmte Weise auf verschiedene Argumente zu reagieren. Versuchen wir, unserenPaymentGateway-Sock dazu zu bringen,true zurückzugeben, wenn wir eine Zahlung von20: leisten

given:
    paymentGateway.makePayment(20) >> true

when:
    def result = paymentGateway.makePayment(20)

then:
    result == true

Interessant ist hier, wie Spock die Überladung von Groovys Operatoren nutzt, um Methodenaufrufe zu stoppen. Mit Java müssen wir echte Methoden aufrufen, was wohl bedeutet, dass der resultierende Code ausführlicher und möglicherweise weniger aussagekräftig ist.

Versuchen wir nun ein paar weitere Arten von Stubbing.

Wenn wir uns nicht mehr um unser Methodenargument kümmern und immertrue, zurückgeben wollten, könnten wir einfach einen Unterstrich verwenden:

paymentGateway.makePayment(_) >> true

Wenn wir zwischen verschiedenen Antworten wechseln möchten, können wir eine Liste bereitstellen, für die jedes Element nacheinander zurückgegeben wird:

paymentGateway.makePayment(_) >>> [true, true, false, true]

Es gibt mehr Möglichkeiten, und diese werden möglicherweise in einem späteren Artikel über das Verspotten behandelt.

5.4. Nachprüfung

Eine andere Sache, die wir mit Mocks machen möchten, ist zu behaupten, dass verschiedene Methoden mit erwarteten Parametern für sie aufgerufen wurden. Mit anderen Worten, wir sollten die Interaktionen mit unseren Mocks überprüfen.

Ein typischer Anwendungsfall für die Überprüfung wäre, wenn eine Methode in unserem Modell einen Rückgabetyp vonvoidhätte. In diesem Fall gibt es kein abgeleitetes Verhalten, das wir mit der zu testenden Methode testen können, da wir kein Ergebnis haben, an dem wir arbeiten können. Wenn etwas zurückgegeben wird, kann die getestete Methode im Allgemeinen damit arbeiten, und das Ergebnis dieser Operation ist das, was wir behaupten.

Versuchen wir zu überprüfen, ob eine Methode mit einem ungültigen Rückgabetyp aufgerufen wird:

def "Should verify notify was called"() {
    given:
        def notifier = Mock(Notifier)

    when:
        notifier.notify('foo')

    then:
        1 * notifier.notify('foo')
}

Spock nutzt die Überlastung des Groovy-Operators erneut. Indem wir unseren Mocks-Methodenaufruf mit einem multiplizieren, geben wir an, wie oft erwartet wird, dass er aufgerufen wurde.

Wenn unsere Methode überhaupt nicht oder nicht so oft wie angegeben aufgerufen worden wäre, hätte unser Test keine aussagekräftige Spock-Fehlermeldung geliefert. Beweisen wir dies, indem wir erwarten, dass es zweimal aufgerufen wurde:

2 * notifier.notify('foo')

Lassen Sie uns anschließend sehen, wie die Fehlermeldung aussieht. Wir werden das wie gewohnt tun. es ist ziemlich informativ:

Too few invocations for:

2 * notifier.notify('foo')   (1 invocation)

Genau wie beim Stubbing können wir auch einen lockereren Überprüfungsabgleich durchführen. Wenn es uns egal wäre, was unser Methodenparameter ist, könnten wir einen Unterstrich verwenden:

2 * notifier.notify(_)

Oder wenn wir sicherstellen möchten, dass es nicht mit einem bestimmten Argument aufgerufen wird, können wir den Operator not verwenden:

2 * notifier.notify(!'foo')

Auch hier gibt es mehr Möglichkeiten, die in einem zukünftigen, weiter fortgeschrittenen Artikel behandelt werden können.

6. Fazit

In diesem Artikel haben wir einen kurzen Überblick über das Testen mit Spock gegeben.

Wir haben gezeigt, wie wir durch die Nutzung von Groovy unsere Tests aussagekräftiger machen können als den typischen JUnit-Stack. Wir haben die Struktur vonspecifications undfeatures erklärt.

Und wir haben gezeigt, wie einfach es ist, datengesteuerte Tests durchzuführen, und wie einfach Verspottungen und Behauptungen über native Spock-Funktionen sind.

Die Implementierung dieser Beispiele kannover on GitHub gefunden werden. Dies ist ein Maven-basiertes Projekt, sollte also so wie es ist einfach auszuführen sein.