Rédaction de spécifications avec Kotlin et Spek

1. Introduction

Les frameworks de tests de spécifications sont complémentaires aux frameworks de tests unitaires pour tester nos applications .

Dans ce didacticiel, nous présenterons le framework Spek - un framework de test de spécification pour Java et Kotlin.

2. Qu'est-ce que le test de spécification?

En termes simples, dans les tests de spécification, nous commençons par la spécification et décrivons l'intention du logiciel, au lieu de sa mécanique.

Ceci est souvent mis à profit dans le développement piloté par le comportement car l'intention est de valider un système par rapport aux spécifications prédéfinies de notre application.

Les cadres de test de spécification couramment connus incluent Spock, Cucumber, Jasmine et RSpec.

2.1. Qu'est-ce que Spek?

Spek est un framework de test de spécification basé sur Kotlin pour la JVM . Il est conçu pour fonctionner comme un moteur de test JUnit 5. Cela signifie que nous pouvons facilement le brancher dans n'importe quel projet qui utilise déjà JUnit 5 pour fonctionner avec tous les autres tests que nous pourrions avoir.

Il est également possible d'exécuter les tests en utilisant l'ancien framework JUnit 4, en utilisant la dépendance JUnit Platform Runner si nécessaire.

2.2. Dépendances de Maven

Pour utiliser Spek, nous devons ajouter les dépendances requises à notre build Maven:

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

La dépendance spek-api est l'API réelle utilisée pour le framework de test. Il définit tout ce avec quoi nos tests fonctionneront. La dépendance spek-junit-platform-engine est alors le moteur de test JUnit 5 nécessaire pour exécuter nos tests.

Notez que toutes les dépendances Spek doivent être de la même version les unes que les autres. La dernière version peut être trouvée ici.

2.3. Premier test

Une fois Spek configuré, écrire des tests est un cas simple d'écriture de la classe correcte dans la structure correcte. Ceci est légèrement inhabituel pour le rendre plus lisible.

Spek exige que nos tests héritent tous d'une superclasse appropriée - typiquement Spek - et que nous implémentions nos tests en passant un bloc au constructeur de cette classe:

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

3. Styles de test

Les tests de spécification mettent l'accent sur l'écriture des tests d'une manière aussi lisible que possible . Cucumber, par exemple, écrit le test entier dans un langage lisible par l'homme, puis le lie à des étapes afin que le code reste séparé.

Spek fonctionne en utilisant des méthodes spéciales qui agissent comme des chaînes lisibles , chacune se voyant attribuer un bloc à exécuter selon le cas. Il existe quelques variations sur les fonctions que nous utilisons en fonction de la façon dont nous voulons que les tests lisent.

3.1. donné / sur / il

Une façon dont nous pouvons écrire nos tests est dans le style «donné / on / it».

Cela utilise des méthodes appelées given , on et it , imbriquées dans cette structure, pour écrire nos tests:

  • donné - définit les conditions initiales du test
  • on - exécute l'action de test
  • it - affirmer que l'action de test s'est effectuée correctement

Nous pouvons avoir autant de blocs que nécessaire, mais nous devons les imbriquer dans cet ordre:

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

Ce test se lit très facilement. En nous concentrant sur les étapes du test, nous pouvons le lire comme «étant donné une calculatrice, en ajoutant 3 et 5, cela produit 8».

3.2. décrire / il

L'autre façon dont nous pouvons écrire nos tests est dans le style «décrire / il». Au lieu de cela, cette méthode utilise la décrivent pour tous l'imbrication, et continue d' utiliser ce pour nos affirmations.

Dans ce cas, nous pouvons imbriquer les méthodes describe autant que nous en avons besoin pour écrire nos tests:

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

Il y a moins de structure appliquée aux tests utilisant ce style, ce qui signifie que nous avons beaucoup plus de flexibilité dans la façon dont nous écrivons les tests.

Malheureusement, l'inconvénient est que les tests ne se lisent pas aussi naturellement que lorsque nous utilisons «given / on / it».

3.3. Styles supplémentaires

Spek n'applique pas ces styles et permettra aux mots-clés d'être interchangés autant que souhaité . Les seules exigences sont que toutes les assertions existent à l'intérieur d'un it et qu'aucun autre bloc ne soit trouvé à ce niveau.

La liste complète des mots-clés d'imbrication disponibles est la suivante:

  • donné
  • sur
  • décris
  • le contexte

Nous pouvons les utiliser pour donner à nos tests la meilleure structure possible pour la manière dont nous voulons les écrire.

3.4. Tests basés sur les données

Le mécanisme utilisé pour définir les tests n'est rien de plus que de simples appels de fonction. Cela signifie que nous pouvons faire d'autres choses avec eux, comme n'importe quel code normal. En particulier, nous pouvons les appeler d'une manière basée sur les données si nous le souhaitons .

La façon la plus simple de le faire est de boucler sur les données que nous voulons utiliser et d'appeler le bloc approprié à l'intérieur de cette boucle:

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.

Enfin, des extraits de code, comme toujours, peuvent être trouvés sur GitHub.