Scrivere le specifiche con Kotlin e Spek

1. Introduzione

I framework di test delle specifiche sono complementari ai framework di test unitari per testare le nostre applicazioni .

In questo tutorial, introdurremo il framework Spek, un framework di test delle specifiche per Java e Kotlin.

2. Che cos'è il test delle specifiche?

In poche parole, in Test delle specifiche, iniziamo con le specifiche e descriviamo l'intenzione del software, invece dei suoi meccanismi.

Questo è spesso sfruttato nello sviluppo guidato dal comportamento poiché l'intenzione è di convalidare un sistema rispetto alle specifiche predefinite della nostra applicazione.

I framework di test delle specifiche comunemente noti includono Spock, Cucumber, Jasmine e RSpec.

2.1. Cos'è Spek?

Spek è un framework di test delle specifiche basato su Kotlin per JVM . È progettato per funzionare come un motore di prova JUnit 5. Ciò significa che possiamo collegarlo facilmente a qualsiasi progetto che già utilizza JUnit 5 per essere eseguito insieme a qualsiasi altro test che potremmo avere.

È anche possibile eseguire i test utilizzando il vecchio framework JUnit 4, utilizzando la dipendenza JUnit Platform Runner, se necessario.

2.2. Dipendenze di Maven

Per usare Spek, dobbiamo aggiungere le dipendenze richieste alla nostra build Maven:

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

La dipendenza spek-api è l'API effettiva utilizzata per il framework di test. Definisce tutto ciò con cui lavoreranno i nostri test. La dipendenza spek-junit-platform-engine è quindi il motore di test JUnit 5 necessario per eseguire i nostri test.

Notare che tutte le dipendenze di Spek devono essere della stessa versione l'una dell'altra. L'ultima versione può essere trovata qui.

2.3. Primo test

Una volta impostato Spek, la scrittura di test è un semplice caso di scrittura della classe corretta nella struttura corretta. Questo è leggermente insolito per renderlo più leggibile.

Spek richiede che tutti i nostri test ereditino da una superclasse appropriata, tipicamente Spek , e che implementiamo i nostri test passando un blocco al costruttore di questa classe:

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

3. Stili di prova

Il test delle specifiche enfatizza la scrittura di test in un modo il più leggibile possibile . Cucumber, ad esempio, scrive l'intero test in un linguaggio leggibile dall'uomo e poi lo lega ai passaggi in modo che il codice venga mantenuto separato.

Spek funziona utilizzando metodi speciali che agiscono come stringhe leggibili , a ciascuna delle quali viene assegnato un blocco da eseguire come appropriato. Ci sono alcune variazioni sulle funzioni che usiamo a seconda del modo in cui vogliamo che i test leggano.

3.1. dato / su / esso

Un modo in cui possiamo scrivere i nostri test è nello stile "dato / su / esso".

Questo metodo usi chiamato dato , su e si , annidate in quella struttura, a scrivere le nostre prove:

  • dato - imposta le condizioni iniziali per il test
  • on - esegue l'azione di prova
  • it - afferma che l'azione di test è stata eseguita correttamente

Possiamo avere tutti i blocchi di cui abbiamo bisogno, ma dobbiamo annidarli in questo ordine:

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) } } } })

Questo test si legge molto facilmente. Concentrandoci sui passaggi del test, possiamo leggerlo come "Data una calcolatrice, aggiungendo 3 e 5 produce 8".

3.2. descrivere / it

L'altro modo in cui possiamo scrivere i nostri test è nello stile "descrivilo". Invece, questo utilizza il metodo descritto per tutta la nidificazione, e mantiene con esso per le nostre affermazioni.

In questo caso, possiamo annidare i metodi di descrizione tanto quanto abbiamo bisogno di scrivere i nostri test:

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) } } } })

C'è meno struttura applicata ai test che utilizzano questo stile, il che significa che abbiamo molta più flessibilità nel modo in cui scriviamo i test.

Sfortunatamente, lo svantaggio di questo è che i test non leggono in modo naturale come quando usiamo "dato / su / esso".

3.3. Stili aggiuntivi

Spek non applica questi stili e consentirà di scambiare le parole chiave quanto desiderato . Gli unici requisiti sono che tutte le asserzioni esistano all'interno di un it e che non siano presenti altri blocchi a quel livello.

L'elenco completo delle parole chiave di nidificazione disponibili è:

  • dato
  • sopra
  • descrivere
  • contesto

Possiamo usarli per dare ai nostri test la migliore struttura possibile per come vogliamo scriverli.

3.4. Test basati sui dati

I meccanismi utilizzati per definire i test non sono altro che semplici chiamate di funzioni. Ciò significa che possiamo fare altre cose con loro, come qualsiasi codice normale. In particolare, se lo desideriamo, possiamo chiamarli in modo basato sui dati .

Il modo più semplice per farlo è scorrere i dati che vogliamo utilizzare e chiamare il blocco appropriato dall'interno di questo ciclo:

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.

Infine, gli snippet di codice, come sempre, possono essere trovati su GitHub.