Einführung in das Testen mit Spock und Groovy

1. Einleitung

In diesem Artikel werfen wir einen Blick auf Spock, ein Groovy-Testframework. 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. Zusätzlich zur Interoperabilität bietet es zusätzliche Sprachkonzepte wie Dynamik, optionale Typen und Metaprogrammierung.

Durch die Verwendung von Groovy führt Spock neue und ausdrucksstarke Methoden zum Testen unserer Java-Anwendungen ein, die im normalen 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 unsere Maven-Abhängigkeiten 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 gmavenplus- Plugin einbinden , 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 wir Groovy und Spock nur zu Testzwecken verwenden. Aus diesem Grund gelten diese Abhängigkeiten für Tests.

3. Struktur eines Spock-Tests

3.1. Technische Daten und Merkmale

Während wir unsere Tests in Groovy schreiben, müssen wir sie anstelle von src / test / java zum Verzeichnis src / test / groovy hinzufügen . Lassen Sie uns unseren ersten Test in diesem Verzeichnis erstellen und ihn Specification.groovy nennen:

class FirstSpecification extends Specification { }

Beachten Sie, dass wir die Spezifikationsschnittstelle erweitern . Jede Spock-Klasse muss dies erweitern, um das Framework für sie verfügbar zu machen. Auf diese Weise können wir unsere erste Funktion implementieren :

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

Bevor wir den Code erklären, ist es auch erwähnenswert, dass das, was wir als Feature bezeichnen , in Spock ein Synonym für das ist, was wir in JUnit als Test betrachten . Also , wenn wir zu einem beziehen Merkmal beziehen wir uns tatsächlich auf einen Test.

Lassen Sie uns nun unsere Funktion analysieren . Dabei sollten wir sofort einige Unterschiede zwischen ihm und Java erkennen können.

Der erste Unterschied besteht darin, dass der Name der Feature-Methode als normale Zeichenfolge geschrieben wird. In JUnit hätten wir einen Methodennamen gehabt, der Camelcase oder Unterstriche verwendet, um die Wörter zu trennen, was nicht so ausdrucksstark oder menschlich lesbar gewesen wäre.

Der nächste ist, dass unser Testcode in einem Erwartungsblock lebt . Wir werden Blöcke in Kürze ausführlicher behandeln, 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. Das liegt daran, dass die Behauptung implizit ist, bestanden wird, wenn unsere Aussage gleich wahr ist, und fehlgeschlagen, wenn sie gleich falsch 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 angegebenen Teile dann mit 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. Blöcke sind eine native Spock-Methode, um die Phasen unseres Tests mithilfe von Etiketten aufzuteilen. Sie geben uns Etiketten für gegeben, wenn dann und mehr:

  1. Setup (Alias ​​durch Given) - Hier führen wir alle erforderlichen Setups durch, bevor ein Test ausgeführt wird. Dies ist ein impliziter Block, zu dem Code in keinem Block gehört
  2. Wann - Hier geben wir einen Anreiz für das, was getestet wird. Mit anderen Worten, wo wir unsere zu testende Methode aufrufen
  3. Dann - Hier gehören die Behauptungen hin. In Spock werden diese als einfache boolesche Behauptungen ausgewertet, die später behandelt werden
  4. Erwarten - Dies ist eine Möglichkeit, unseren Reiz und unsere Behauptung innerhalb desselben Blocks auszuführen. Je nachdem, was wir ausdrucksstärker finden, können wir diesen Block verwenden oder nicht
  5. Bereinigung - Hier werden alle Ressourcen für Testabhängigkeiten 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, helfen Blöcke unserem Test, besser lesbar zu werden.

3.3. Groovy-Funktionen für Behauptungen nutzen

Innerhalb der Then- und Expect- Blöcke sind Aussagen implizit .

Meistens wird jede Aussage ausgewertet und schlägt dann fehl, wenn sie nicht wahr ist . Wenn Sie dies mit verschiedenen Groovy-Funktionen koppeln, wird die Notwendigkeit einer Assertionsbibliothek deutlich. Versuchen wir eine Listenaussage , 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 wird eine Liste 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 die nützlichste Funktion die Überlastung des Bedieners. Dies bedeutet, dass intern anstelle eines Referenzvergleichs wie in Java die Methode equals () aufgerufen wird, um die beiden Listen zu vergleichen.

Es lohnt sich auch zu demonstrieren, was passiert, wenn unser Test fehlschlägt. Lassen Sie es uns kaputt machen und dann sehen, 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 in zwei Listen nur equals () aufgerufen wird, ist Spock intelligent genug, um eine Aufschlüsselung der fehlgeschlagenen Behauptung durchzuführen und uns nützliche Informationen zum 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 einen Try-Catch- Block, deklarieren zu Beginn unseres Tests die Erwartung oder verwenden eine Bibliothek eines Drittanbieters. Spocks native Behauptungen beinhalten eine Möglichkeit, Ausnahmen sofort zu behandeln:

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 Methode throw () 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?

Datengesteuertes Testen ist im Wesentlichen, wenn wir dasselbe Verhalten mehrmals mit unterschiedlichen Parametern und Behauptungen testen . Ein klassisches Beispiel hierfür wäre das Testen einer mathematischen Operation wie das Quadrieren einer Zahl. Abhängig von den verschiedenen Permutationen von Operanden ist das Ergebnis unterschiedlich. In Java ist der Begriff, mit dem wir vielleicht besser vertraut sind, parametrisiertes 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 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

One easy win for Spock when compared to JUnit is how it cleanly it implements parameterized tests. Again, in Spock, this is known as Data Driven Testing. Now, let's implement the same test again, only this time we'll use Spock with Data Tables, which provides a far more convenient way of performing a parameterized test:

def "numbers to the power of two"(int a, int b, int c)  4 3 

As we can see, we just have a straightforward and expressive Data table containing all our parameters.

Also, it belongs where it should do, alongside the test, and there is no boilerplate. The test is expressive, with a human-readable name, and pure expect and where block to break up the logical sections.

4.4. When a Datatable Fails

It's also worth seeing what happens when our test fails:

Condition not satisfied: Math.pow(a, b) == c | | | | | 4.0 2 2 | 1 false Expected :1 Actual :4.0

Again, Spock gives us a very informative error message. We can see exactly what row of our Datatable caused a failure and why.

5. Mocking

5.1. What Is Mocking?

Mocking is a way of changing the behavior of a class which our service under test collaborates with. It's a helpful way of being able to test business logic in isolation of its dependencies.

A classic example of this would be replacing a class which makes a network call with something which simply pretends to. For a more in-depth explanation, it's worth reading this article.

5.2. Mocking Using Spock

Spock has it's own mocking framework, making use of interesting concepts brought to the JVM by Groovy. First, let's instantiate a Mock:

PaymentGateway paymentGateway = Mock()

In this case, the type of our mock is inferred by the variable type. As Groovy is a dynamic language, we can also provide a type argument, allow us to not have to assign our mock to any particular type:

def paymentGateway = Mock(PaymentGateway)

Now, whenever we call a method on our PaymentGateway mock, a default response will be given, without a real instance being invoked:

when: def result = paymentGateway.makePayment(12.99) then: result == false

The term for this is lenient mocking. This means that mock methods which have not been defined will return sensible defaults, as opposed to throwing an exception. This is by design in Spock, in order to make mocks and thus tests less brittle.

5.3. Stubbing Method Calls on Mocks

We can also configure methods called on our mock to respond in a certain way to different arguments. Let's try getting our PaymentGateway mock to return true when we make a payment of 20:

given: paymentGateway.makePayment(20) >> true when: def result = paymentGateway.makePayment(20) then: result == true

What's interesting here, is how Spock makes use of Groovy's operator overloading in order to stub method calls. With Java, we have to call real methods, which arguably means that the resulting code is more verbose and potentially less expressive.

Now, let's try a few more types of stubbing.

If we stopped caring about our method argument and always wanted to return true, we could just use an underscore:

paymentGateway.makePayment(_) >> true

If we wanted to alternate between different responses, we could provide a list, for which each element will be returned in sequence:

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

There are more possibilities, and these may be covered in a more advanced future article on mocking.

5.4. Verification

Another thing we might want to do with mocks is assert that various methods were called on them with expected parameters. In other words, we ought to verify interactions with our mocks.

A typical use case for verification would be if a method on our mock had a void return type. In this case, by there being no result for us to operate on, there's no inferred behavior for us to test via the method under test. Generally, if something was returned, then the method under test could operate on it, and it's the result of that operation would be what we assert.

Let's try verifying that a method with a void return type is called:

def "Should verify notify was called"() { given: def notifier = Mock(Notifier) when: notifier.notify('foo') then: 1 * notifier.notify('foo') } 

Spock is leveraging Groovy operator overloading again. By multiplying our mocks method call by one, we are saying how many times we expect it to have been called.

If our method had not been called at all or alternatively had not been called as many times as we specified, then our test would have failed to give us an informative Spock error message. Let's prove this by expecting it to have been called twice:

2 * notifier.notify('foo')

Following this, let's see what the error message looks like. We'll that as usual; it's quite informative:

Too few invocations for: 2 * notifier.notify('foo') (1 invocation)

Just like stubbing, we can also perform looser verification matching. If we didn't care what our method parameter was, we could use an underscore:

2 * notifier.notify(_)

Or if we wanted to make sure it wasn't called with a particular argument, we could use the not operator:

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 der Spezifikationen und Funktionen erläutert .

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 finden Sie auf GitHub. Dies ist ein Maven-basiertes Projekt und sollte daher so wie es ist einfach auszuführen sein.