Introduction aux tests avec Spock et Groovy

1. Introduction

Dans cet article, nous allons jeter un oeil à Spock, un framework de test Groovy. Principalement, Spock vise à être une alternative plus puissante à la pile JUnit traditionnelle, en tirant parti des fonctionnalités de Groovy.

Groovy est un langage basé sur JVM qui s'intègre parfaitement à Java. En plus de l'interopérabilité, il offre des concepts de langage supplémentaires tels que le fait d'être dynamique, d'avoir des types optionnels et de la méta-programmation.

En utilisant Groovy, Spock introduit de nouvelles façons expressives de tester nos applications Java, ce qui n'est tout simplement pas possible dans le code Java ordinaire. Nous explorerons certains des concepts de haut niveau de Spock au cours de cet article, avec quelques exemples pratiques étape par étape.

2. Dépendance de Maven

Avant de commencer, ajoutons nos dépendances Maven:

 org.spockframework spock-core 1.0-groovy-2.4 test   org.codehaus.groovy groovy-all 2.4.7 test 

Nous avons ajouté Spock et Groovy comme nous le ferions pour n'importe quelle bibliothèque standard. Cependant, comme Groovy est un nouveau langage JVM, nous devons inclure le plugin gmavenplus afin de pouvoir le compiler et l'exécuter:

 org.codehaus.gmavenplus gmavenplus-plugin 1.5    compile testCompile    

Nous sommes maintenant prêts à écrire notre premier test Spock, qui sera écrit en code Groovy. Notez que nous utilisons Groovy et Spock uniquement à des fins de test et c'est pourquoi ces dépendances ont une portée de test.

3. Structure d'un test Spock

3.1. Spécifications et caractéristiques

Alors que nous écrivons nos tests dans Groovy, nous devons les ajouter au répertoire src / test / groovy , au lieu de src / test / java. Créons notre premier test dans ce répertoire, en le nommant Specification.groovy:

class FirstSpecification extends Specification { }

Notez que nous étendons l' interface de spécification . Chaque classe Spock doit étendre cela afin de rendre le framework disponible. C'est ce qui nous permet de mettre en œuvre notre première fonctionnalité:

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

Avant d'expliquer le code, il convient également de noter que dans Spock, ce que nous appelons une fonctionnalité est un peu synonyme de ce que nous considérons comme un test dans JUnit. Ainsi, chaque fois que nous nous référons à une fonctionnalité, nous faisons référence à un test.

Maintenant, analysons notre fonctionnalité . Ce faisant, nous devrions immédiatement être en mesure de voir certaines différences entre lui et Java.

La première différence est que le nom de la méthode de fonctionnalité est écrit sous la forme d'une chaîne ordinaire. Dans JUnit, nous aurions eu un nom de méthode qui utilise camelcase ou des traits de soulignement pour séparer les mots, ce qui n'aurait pas été aussi expressif ou lisible par l'homme.

Le suivant est que notre code de test vit dans un bloc expect . Nous aborderons les blocs plus en détail sous peu, mais ils sont essentiellement une manière logique de fractionner les différentes étapes de nos tests.

Enfin, on se rend compte qu'il n'y a pas d'affirmations. C'est parce que l'assertion est implicite, passant lorsque notre déclaration est égale à true et échoue lorsqu'elle est égale à false . Encore une fois, nous couvrirons les affirmations plus en détail sous peu.

3.2. Blocs

Parfois, lors de l'écriture d'un test JUnit, nous pouvons remarquer qu'il n'y a pas de moyen expressif de le diviser en plusieurs parties. Par exemple, si nous suivions le développement axé sur le comportement, nous pourrions finir par désigner les parties données quand puis utiliser des commentaires:

@Test public void givenTwoAndTwo_whenAdding_thenResultIsFour() { // Given int first = 2; int second = 4; // When int result = 2 + 2; // Then assertTrue(result == 4) }

Spock résout ce problème avec des blocs. Les blocs sont une manière native de Spock de fractionner les phases de notre test à l'aide d'étiquettes. Ils nous donnent des étiquettes pour donné quand alors et plus encore:

  1. Configuration (aliasée par donné) - Ici, nous effectuons toute configuration nécessaire avant l'exécution d'un test. Il s'agit d'un bloc implicite, le code n'appartenant à aucun bloc en faisant partie
  2. Quand - C'est là que nous stimulons ce qui est testé. En d'autres termes, où nous invoquons notre méthode sous test
  3. Alors - C'est là que appartiennent les affirmations. Dans Spock, celles-ci sont évaluées comme des assertions booléennes simples, qui seront couvertes plus tard
  4. Attendez - vous - C'est une façon d'exécuter notre stimulus et notre affirmation dans le même bloc. En fonction de ce que nous trouvons plus expressif, nous pouvons ou non choisir d'utiliser ce bloc
  5. Nettoyage - Ici, nous supprimons toutes les ressources de dépendance de test qui seraient autrement laissées pour compte. Par exemple, nous pourrions vouloir supprimer tous les fichiers du système de fichiers ou supprimer les données de test écrites dans une base de données

Essayons à nouveau de mettre en œuvre notre test, cette fois en utilisant pleinement les blocs:

def "two plus two should equal four"() { given: int left = 2 int right = 2 when: int result = left + right then: result == 4 }

Comme nous pouvons le voir, les blocs aident notre test à devenir plus lisible.

3.3. Tirer parti des fonctionnalités Groovy pour les assertions

Dans les blocs then et expect , les assertions sont implicites .

La plupart du temps, chaque instruction est évaluée et échoue si elle n'est pas vraie . En couplant cela avec diverses fonctionnalités de Groovy, cela supprime le besoin d'une bibliothèque d'assertions. Essayons une assertion de liste pour démontrer ceci:

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

Bien que nous n'abordions que brièvement Groovy dans cet article, cela vaut la peine d'expliquer ce qui se passe ici.

Premièrement, Groovy nous donne des moyens plus simples de créer des listes. Nous pouvons simplement déclarer nos éléments avec des crochets, et en interne une liste sera instanciée.

Deuxièmement, comme Groovy est dynamique, nous pouvons utiliser def, ce qui signifie simplement que nous ne déclarons pas de type pour nos variables.

Enfin, dans le cadre de la simplification de notre test, la fonctionnalité la plus utile démontrée est la surcharge des opérateurs. Cela signifie qu'en interne, plutôt que de faire une comparaison de référence comme en Java, la méthode equals () sera invoquée pour comparer les deux listes.

Cela vaut également la peine de démontrer ce qui se passe lorsque notre test échoue. Faisons une pause, puis visualisons ce qui est sorti sur la console:

Condition not satisfied: list == [1, 3, 4] | | | false [2, 3, 4]  at FirstSpecification.Should be able to remove from list(FirstSpecification.groovy:30)

Alors que tout ce qui se passe est d'appeler equals () sur deux listes, Spock est suffisamment intelligent pour effectuer une ventilation de l'assertion défaillante, nous donnant des informations utiles pour le débogage.

3.4. Faire valoir des exceptions

Spock also provides us with an expressive way of checking for exceptions. In JUnit, some our options might be using a try-catch block, declare expected at the top of our test, or making use of a third party library. Spock's native assertions come with a way of dealing with exceptions out of the box:

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 }

Here, we've not had to introduce an additional library. Another advantage is that the thrown() method will assert the type of the exception, but not halt execution of the test.

4. Data Driven Testing

4.1. What Is a Data Driven Testing?

Essentially, data driven testing is when we test the same behavior multiple times with different parameters and assertions. A classic example of this would be testing a mathematical operation such as squaring a number. Depending on the various permutations of operands, the result will be different. In Java, the term we may be more familiar with is parameterized testing.

4.2. Implementing a Parameterized Test in Java

For some context, it's worth implementing a parameterized test using JUnit:

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

As we can see there's quite a lot of verbosity, and the code isn't very readable. We've had to create a two-dimensional object array that lives outside of the test, and even a wrapper object for injecting the various test values.

4.3. Using Datatables 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')

Encore une fois, il y a plus de possibilités, qui pourraient être couvertes dans un prochain article plus avancé.

6. Conclusion

Dans cet article, nous avons donné un aperçu rapide des tests avec Spock.

Nous avons démontré comment, en tirant parti de Groovy, nous pouvons rendre nos tests plus expressifs que la pile JUnit typique. Nous avons expliqué la structure des spécifications et des fonctionnalités .

Et nous avons montré à quel point il est facile d'effectuer des tests basés sur les données, et aussi à quel point les moqueries et les assertions sont faciles via la fonctionnalité Spock native.

L'implémentation de ces exemples est disponible à l'adresse over sur GitHub. Il s'agit d'un projet basé sur Maven, il devrait donc être facile à exécuter tel quel.