Schreibspezifikationen mit Kotlin und Spek

1. Einleitung

Spezifikationsprüfungs-Frameworks ergänzen Unit Unit-Test-Frameworks zum Testen unserer Anwendungen .

In diesem Tutorial stellen wir das Spek-Framework vor - ein Framework für Spezifikationstests für Java und Kotlin.

2. Was ist Spezifikationstest?

Einfach ausgedrückt, beginnen wir beim Spezifikationstest mit der Spezifikation und beschreiben die Absicht der Software anstelle ihrer Mechanik.

Dies wird häufig in der verhaltensgesteuerten Entwicklung genutzt, da ein System anhand vordefinierter Spezifikationen unserer Anwendung validiert werden soll.

Zu den allgemein bekannten Frameworks für Spezifikationstests gehören Spock, Cucumber, Jasmine und RSpec.

2.1. Was ist Spek?

Spek ist ein Kotlin-basiertes Framework für Spezifikationstests für die JVM . Es wurde als JUnit 5-Test-Engine entwickelt. Dies bedeutet, dass wir es problemlos in jedes Projekt einbinden können, das bereits JUnit 5 verwendet, um es zusammen mit anderen Tests auszuführen, die wir möglicherweise haben.

Es ist auch möglich, die Tests mit dem älteren JUnit 4-Framework auszuführen, indem bei Bedarf die Abhängigkeit von JUnit Platform Runner verwendet wird.

2.2. Maven-Abhängigkeiten

Um Spek verwenden zu können, müssen wir unserem Maven-Build die erforderlichen Abhängigkeiten hinzufügen:

 org.jetbrains.spek spek-api 1.1.5 test   org.jetbrains.spek spek-junit-platform-engine 1.1.5 test 

Die spek-api- Abhängigkeit ist die tatsächliche API, die für das Testframework verwendet wird. Es definiert alles, mit dem unsere Tests arbeiten werden. Die Abhängigkeit von spek-junit-platform-engine ist dann die JUnit 5-Test-Engine, die zur Ausführung unserer Tests benötigt wird.

Beachten Sie, dass alle Spek-Abhängigkeiten dieselbe Version haben müssen. Die neueste Version finden Sie hier.

2.3. Erster Test

Sobald Spek eingerichtet ist, ist das Schreiben von Tests ein einfacher Fall, bei dem die richtige Klasse in der richtigen Struktur geschrieben wird. Dies ist etwas ungewöhnlich, um die Lesbarkeit zu verbessern.

Spek erfordert, dass unsere Tests alle von einer geeigneten Oberklasse erben - normalerweise Spek - und dass wir unsere Tests implementieren, indem wir einen Block an den Konstruktor dieser Klasse übergeben:

class FirstSpec : Spek({ // Implement the test here })

3. Teststile

Beim Testen von Spezifikationen wird Wert darauf gelegt, Tests so lesbar wie möglich zu schreiben . Cucumber schreibt beispielsweise den gesamten Test in einer für Menschen lesbaren Sprache und verknüpft ihn dann mit Schritten, sodass der Code getrennt bleibt.

Spek arbeitet mit speziellen Methoden, die als lesbare Zeichenfolgen fungieren , von denen jede einen Block erhält, der entsprechend ausgeführt werden kann. Es gibt einige Variationen darüber, welche Funktionen wir verwenden, je nachdem, wie die Tests gelesen werden sollen.

3.1. gegeben / auf / es

Eine Möglichkeit, unsere Tests zu schreiben, ist der "Given / On / It" -Stil.

Diese nutzen Methoden genannt gegeben , auf und es in dieser Struktur verschachtelt, unsere Tests zu schreiben:

  • gegeben - legt die Anfangsbedingungen für den Test fest
  • on - Führen Sie die Testaktion aus
  • it - behaupten, dass die Testaktion korrekt ausgeführt wurde

Wir können von jedem Block so viele haben, wie wir brauchen, müssen sie aber in dieser Reihenfolge verschachteln:

class CalculatorTest : Spek({ given("A calculator") { val calculator = Calculator() on("Adding 3 and 5") { val result = calculator.add(3, 5) it("Produces 8") { assertEquals(8, result) } } } })

Dieser Test liest sich sehr leicht. Wenn wir uns auf die Testschritte konzentrieren, können wir es als „Wenn ein Taschenrechner gegeben ist und 3 und 5 addiert, ergibt sich 8“ lesen.

3.2. beschreiben / es

Die andere Art, wie wir unsere Tests schreiben können, ist der "beschreiben / es" -Stil. Stattdessen Diese verwendet das Verfahren beschreiben , für alle der Verschachtelung und hält mit Hilfe es für unsere Behauptungen.

In diesem Fall können wir die Beschreibungsmethoden so weit verschachteln, wie wir zum Schreiben unserer Tests benötigen:

class CalculatorTest : Spek({ describe("A calculator") { val calculator = Calculator() describe("Addition") { val result = calculator.add(3, 5) it("Produces the correct answer") { assertEquals(8, result) } } } })

Bei den Tests, die diesen Stil verwenden, wird weniger Struktur erzwungen, was bedeutet, dass wir viel flexibler beim Schreiben der Tests sind.

Leider ist der Nachteil dabei, dass die Tests nicht so natürlich gelesen werden, wie wenn wir "Given / On / It" verwenden.

3.3. Zusätzliche Stile

Spek erzwingt diese Stile nicht und ermöglicht es, die Schlüsselwörter so oft wie gewünscht auszutauschen . Die einzigen Anforderungen sind, dass alle Zusicherungen innerhalb eines It existieren und dass auf dieser Ebene keine anderen Blöcke gefunden werden.

Die vollständige Liste der verfügbaren Verschachtelungsschlüsselwörter lautet:

  • gegeben
  • auf
  • beschreiben
  • Kontext

Wir können diese verwenden, um unseren Tests die bestmögliche Struktur zu geben, wie wir sie schreiben möchten.

3.4. Datengesteuerte Tests

Der Mechanismus zum Definieren von Tests ist nichts anderes als einfache Funktionsaufrufe. Dies bedeutet, dass wir mit ihnen andere Dinge tun können, wie mit jedem normalen Code. Insbesondere können wir sie auf Wunsch datengesteuert aufrufen .

Der einfachste Weg, dies zu tun, besteht darin, die Daten, die wir verwenden möchten, zu durchlaufen und den entsprechenden Block innerhalb dieser Schleife aufzurufen:

class DataDrivenTest : Spek({ describe("A data driven test") { mapOf( "hello" to "HELLO", "world" to "WORLD" ).forEach { input, expected -> describe("Capitalising $input") { it("Correctly returns $expected") { assertEquals(expected, input.toUpperCase()) } } } } })

We can do all sorts of things like this if we need to, but this is likely the most useful.

4. Assertions

Spek doesn't prescribe any particular way of using assertions. Instead, it allows us to use whatever assertion framework we're most comfortable with.

The obvious choice will be the org.junit.jupiter.api.Assertions class, since we're already using the JUnit 5 framework as our test runner.

However, we can also use any other assertion library that we want if it makes our tests better – e.g., Kluent, Expekt or HamKrest.

The benefit of using these libraries instead of the standard JUnit 5 Assertions class is down to the readability of the tests.

For example, the above test re-written using Kluent reads as:

class CalculatorTest : Spek({ describe("A calculator") { val calculator = Calculator() describe("Addition") { val result = calculator.add(3, 5) it("Produces the correct answer") { result shouldEqual 8 } } } })

5. Before/After Handlers

As with most test frameworks, Spek can also execute logic before/after tests.

These are, exactly as their name implies, blocks that are executed before or after the test itself.

The options here are:

  • beforeGroup
  • afterGroup
  • beforeEachTest
  • afterEachTest

These can be placed in any of the nesting keywords and will apply to everything inside that group.

The way Spek works, all code inside any of the nesting keywords is executed immediately on the start of the test, but the control blocks are executed in a particular order centered around the it blocks.

Working from the outside-in, Spek will execute each beforeEachTest block immediately before every it block nested within the same group, and each afterEachTest block immediately after every it block. Equally, Spek will execute each beforeGroup block immediately before every group and each afterGroup block immediately after every group in the current nesting.

This is complicated, and is best explained with an example:

class GroupTest5 : Spek({ describe("Outer group") { beforeEachTest { System.out.println("BeforeEachTest 0") } beforeGroup { System.out.println("BeforeGroup 0") } afterEachTest { System.out.println("AfterEachTest 0") } afterGroup { System.out.println("AfterGroup 0") } describe("Inner group 1") { beforeEachTest { System.out.println("BeforeEachTest 1") } beforeGroup { System.out.println("BeforeGroup 1") } afterEachTest { System.out.println("AfterEachTest 1") } afterGroup { System.out.println("AfterGroup 1") } it("Test 1") { System.out.println("Test 1") } } } })

The output of running the above is:

BeforeGroup 0 BeforeGroup 1 BeforeEachTest 0 BeforeEachTest 1 Test 1 AfterEachTest 1 AfterEachTest 0 AfterGroup 1 AfterGroup 0

Straight away we can see that the outer beforeGroup/afterGroup blocks are around the entire set of tests, whilst the inner beforeGroup/afterGroup blocks are only around the tests in the same context.

We can also see that all of the beforeGroup blocks are executed before any beforeEachTest blocks and the opposite for afterGroup/afterEachTest.

A larger example of this, showing the interaction between multiple tests in multiple groups, can be seen on GitHub.

6. Test Subjects

Many times, we will be writing a single Spec for a single Test Subject. Spek offers a convenient way to write this, such that it manages the Subject Under Test for us automatically. We use the SubjectSpek base class instead of the Spek class for this.

When we use this, we need to declare a call to the subject block at the outermost level. This defines the test subject. We can then refer to this from any of our test code as subject.

We can use this to re-write our earlier calculator test as follows:

class CalculatorTest : SubjectSpek({ subject { Calculator() } describe("A calculator") { describe("Addition") { val result = subject.add(3, 5) it("Produces the correct answer") { assertEquals(8, result) } } } })

It may not seem like much, but this can help to make the tests a lot more readable, especially when there are a large number of test cases to consider.

6.1. Maven Dependencies

To use the Subject Extension, we need to add a dependency to our Maven build:

 org.jetbrains.spek spek-subject-extension 1.1.5 test 

7. Summary

Spek is a powerful framework allowing for some very readable tests, which in turn means that all parts of the organization can read them.

This is important to allow all colleagues to contribute towards testing the entire application.

Schließlich können Code-Schnipsel wie immer auf GitHub gefunden werden.