Introduction aux tests avec Spock et Groovy

Introduction aux tests avec Spock et Groovy

1. introduction

Dans cet article, nous allons examinerSpock, un cadre de test deGroovy. Spock vise principalement à être une alternative plus puissante à la pile JUnit traditionnelle, en exploitant les fonctionnalités de Groovy.

Groovy est un langage basé sur la machine virtuelle Java qui s'intègre parfaitement à Java. En plus de l'interopérabilité, il offre des concepts de langage supplémentaires, tels que la dynamique, les types facultatifs et 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 allons explorer certains des concepts de haut niveau de Spock au cours de cet article, avec quelques exemples pratiques étape par étape.

2. Dépendance Maven

Avant de commencer, ajoutons nosMaven dependencies:


    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 plugingmavenplus 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 n’utilisons Groovy et Spock qu’à des fins de test. C’est pourquoi ces dépendances sont testées.

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épertoiresrc/test/groovy, au lieu desrc/test/java. Créons notre premier test dans ce répertoire, en le nommantSpecification.groovy:

class FirstSpecification extends Specification {

}

Notez que nous étendons l'interfaceSpecification. Chaque classe Spock doit l'étendre pour pouvoir utiliser le framework. C’est ce qui nous permet d’implémenter nos premiersfeature:

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 unfeature est un peu synonyme de ce que nous considérons comme untest dans JUnit. Doncwhenever we refer to a feature we are actually referring to a test.

Maintenant, analysons nosfeature. Ce faisant, nous devrions immédiatement être en mesure de voir quelques différences entre ce logiciel et Java.

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

Le suivant est que notre code de test vit dans un blocexpect. Nous traiterons des blocs plus en détail dans quelques instants, mais ils constituent essentiellement un moyen logique de scinder les différentes étapes de nos tests.

Enfin, nous réalisons qu’il n’ya pas d’affirmations. C'est parce que l'assertion est implicite, passant lorsque notre instruction 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 partiesgiven when then à l'aide de 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. Blocks are a Spock native way of breaking up the phases of our test using labels. Ils nous donnent des étiquettes pourgiven when then et plus:

  1. Setup (Aliased by Given) - Ici, nous effectuons toute configuration nécessaire avant l'exécution d'un test. Il s’agit d’un bloc implicite, le code n’en faisant pas partie du tout.

  2. When - C'est ici que nous fournissons unstimulus à ce qui est en cours de test. En d'autres termes, où nous appelons notre méthode sous test

  3. Then - C'est là que les assertions appartiennent. Dans Spock, celles-ci sont évaluées comme de simples assertions booléennes, qui seront traitées plus tard

  4. Expect - C'est une manière d'exécuter nosstimulus etassertion dans le même bloc. Selon ce que nous trouvons plus expressif, nous pouvons ou non choisir d’utiliser ce bloc

  5. Cleanup - 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 constater, les blocs aident notre test à devenir plus lisible.

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

Within the then and expect blocks, assertions are implicit.

La plupart du temps, chaque instruction est évaluée puis échoue si ce n'est pastrue. Lorsque vous associez cela à diverses fonctionnalités de Groovy, vous n’avez plus besoin d’une bibliothèque d’assertions. Essayons une assertionlist 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 unlist sera instancié.

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

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

Cela vaut également la peine de montrer ce qui se passe lorsque notre test échoue. Faisons une pause, puis visualisons le contenu de 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'appelerequals() 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 nous fournit également un moyen expressif de vérifier les exceptions. Dans JUnit, certaines de nos options peuvent utiliser un bloctry-catch, déclarerexpected en haut de notre test ou utiliser une bibliothèque tierce. Les assertions natives de Spock permettent de gérer les exceptions dès le départ:

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
}

Ici, nous n'avons pas eu à introduire de bibliothèque supplémentaire. Un autre avantage est que la méthodethrown() affirmera le type de l'exception, mais n'interrompra pas l'exécution du test.

4. Tests basés sur les données

4.1. Qu'est-ce qu'un test basé sur les données?

Essentiellement,data driven testing is when we test the same behavior multiple times with different parameters and assertions. Un exemple classique serait de tester une opération mathématique telle que la quadrature d'un nombre. En fonction des différentes permutations d'opérandes, le résultat sera différent. En Java, le terme avec lequel nous sommes peut-être plus familiers est test paramétré.

4.2. Implémentation d'un test paramétré en Java

Pour certains contextes, il vaut la peine d'implémenter un test paramétré à l'aide de 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));
    }
}

Comme nous pouvons le voir, il y a beaucoup de verbosité et le code n'est pas très lisible. Nous avons dû créer un tableau d’objets à deux dimensions qui vit en dehors du test, et même un objet wrapper pour injecter les différentes valeurs de test.

4.3. Utilisation des tables de données dans Spock

Un avantage facile pour Spock, comparé à JUnit, réside dans la manière dont il implémente proprement des tests paramétrés. Encore une fois, dans Spock, cela s'appelleData Driven Testing. Maintenant, implémentons à nouveau le même test, mais cette fois, nous utiliserons Spock avecData Tables, ce qui fournit un moyen beaucoup plus pratique d'effectuer un test paramétré :

def "numbers to the power of two"(int a, int b, int c) {
  expect:
      Math.pow(a, b) == c

  where:
      a | b | c
      1 | 2 | 1
      2 | 2 | 4
      3 | 2 | 9
  }

Comme nous pouvons le constater, nous n’avons qu’un tableau de données simple et expressif contenant tous nos paramètres.

En outre, il doit être placé comme il se doit, parallèlement à l’essai, et il n’ya pas de passe-partout. Le test est expressif, avec un nom lisible par l'homme et un blocexpect etwhere pur pour diviser les sections logiques.

4.4. Quand une table de données échoue

Cela vaut également la peine de voir ce qui se passe lorsque notre test échoue:

Condition not satisfied:

Math.pow(a, b) == c
     |   |  |  |  |
     4.0 2  2  |  1
               false

Expected :1

Actual   :4.0

Encore une fois, Spock nous donne un message d'erreur très informatif. Nous pouvons voir exactement quelle ligne de notre Datatable a provoqué une défaillance et pourquoi.

5. Railleur

5.1. Qu'est-ce que la moquerie?

Mocking est un moyen de changer le comportement d'une classe avec laquelle notre service testé teste. C'est un moyen utile de pouvoir tester la logique métier indépendamment de ses dépendances.

Un exemple classique serait de remplacer une classe qui effectue un appel réseau par quelque chose qui prétend simplement le faire. Pour une explication plus approfondie, il vaut la peine de lirethis article.

5.2. Se moquer avec Spock

Spock a son propre framework de simulation, utilisant des concepts intéressants apportés à la JVM par Groovy. Tout d'abord, instancions unMock:

PaymentGateway paymentGateway = Mock()

Dans ce cas, le type de notre maquette est déduit du type de variable. Comme Groovy est un langage dynamique, nous pouvons également fournir un argument de type, ce qui nous permet de ne pas avoir à affecter notre modèle à un type particulier:

def paymentGateway = Mock(PaymentGateway)

Maintenant, chaque fois que nous appelons une méthode sur notrePaymentGateway mock,, une réponse par défaut sera donnée, sans qu'une instance réelle ne soit appelée:

when:
    def result = paymentGateway.makePayment(12.99)

then:
    result == false

Le terme pour cela estlenient mocking. Cela signifie que les méthodes fictives qui n'ont pas été définies renverront des valeurs par défaut sensibles, par opposition à une exception. C’est ce qui est prévu dans Spock, afin de créer des simulacres et donc de tester moins de fragilité.

5.3. La méthode de stubbing appelle lesMocks

Nous pouvons également configurer des méthodes appelées sur notre maquette pour répondre d'une certaine manière à différents arguments. Essayons de faire en sorte que notre simulationPaymentGateway renvoietrue lorsque nous effectuons un paiement de20:

given:
    paymentGateway.makePayment(20) >> true

when:
    def result = paymentGateway.makePayment(20)

then:
    result == true

Ce qui est intéressant ici, c’est de savoir comment Spock utilise la surcharge d’opérateurs de Groovy pour stuber les appels de méthode. Avec Java, nous devons appeler de vraies méthodes, ce qui signifie sans doute que le code résultant est plus détaillé et potentiellement moins expressif.

Maintenant, essayons quelques autres types de stubbing.

Si nous arrêtions de nous soucier de notre argument de méthode et voulions toujours renvoyertrue,, nous pourrions simplement utiliser un trait de soulignement:

paymentGateway.makePayment(_) >> true

Si nous voulions alterner entre différentes réponses, nous pourrions fournir une liste, pour laquelle chaque élément sera renvoyé dans l'ordre:

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

Il y a plus de possibilités, et celles-ci peuvent être couvertes dans un futur article plus avancé sur les moqueries.

5.4. Vérification

Une autre chose que nous pourrions vouloir faire avec les simulacres est d’affirmer que différentes méthodes ont été utilisées avec les paramètres attendus. En d'autres termes, nous devrions vérifier les interactions avec nos simulacres.

Un cas d'utilisation typique pour la vérification serait si une méthode sur notre maquette avait un type de retourvoid. Dans ce cas, en l'absence de résultat sur lequel nous pouvons opérer, nous n'avons aucun comportement déduit à tester via la méthode sous test. En général, si quelque chose était renvoyé, la méthode testée pourrait fonctionner dessus, et c’est le résultat de cette opération qui serait ce que nous affirmons.

Essayons de vérifier qu'une méthode avec un type de retour void est appelée:

def "Should verify notify was called"() {
    given:
        def notifier = Mock(Notifier)

    when:
        notifier.notify('foo')

    then:
        1 * notifier.notify('foo')
}

Spock exploite à nouveau la surcharge de l’opérateur Groovy. En multipliant notre méthode de simulation par un, nous disons combien de fois nous nous attendons à ce qu'elle ait été appelée.

Si notre méthode n'avait pas été appelée du tout ou, alternativement, n'avait pas été appelée autant de fois que nous l'avions spécifiée, notre test n'aurait pas réussi à nous donner un message d'erreur informatif Spock. Prouvons cela en nous attendons à ce qu’il ait été appelé deux fois:

2 * notifier.notify('foo')

Ensuite, voyons à quoi ressemble le message d'erreur. Nous le ferons comme d’habitude; c'est assez instructif:

Too few invocations for:

2 * notifier.notify('foo')   (1 invocation)

Tout comme la substitution, nous pouvons également effectuer une correspondance de vérification plus souple. Si nous ne nous soucions pas du paramètre de notre méthode, nous pourrions utiliser un trait de soulignement:

2 * notifier.notify(_)

Ou si nous voulions nous assurer qu'il n'était pas appelé avec un argument particulier, nous pourrions utiliser l'opérateur not:

2 * notifier.notify(!'foo')

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

6. Conclusion

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

Nous avons montré comment, en tirant parti de Groovy, nous pouvons rendre nos tests plus expressifs que la pile JUnit typique. Nous avons expliqué la structure despecifications etfeatures.

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 affirmations sont faciles grâce à la fonctionnalité Spock native.

L'implémentation de ces exemples peut être trouvéeover on GitHub. Ceci est un projet basé sur Maven, il devrait donc être facile à exécuter tel quel.