Introduzione ai test con Spock e Groovy

1. Introduzione

In questo articolo, daremo un'occhiata a Spock, un framework di test Groovy. Principalmente, Spock mira ad essere un'alternativa più potente al tradizionale stack JUnit, sfruttando le funzionalità di Groovy.

Groovy è un linguaggio basato su JVM che si integra perfettamente con Java. Oltre all'interoperabilità, offre concetti di linguaggio aggiuntivi come l'essere dinamico, avere tipi opzionali e meta-programmazione.

Facendo uso di Groovy, Spock introduce modi nuovi ed espressivi per testare le nostre applicazioni Java, che semplicemente non sono possibili nel normale codice Java. Esploreremo alcuni dei concetti di alto livello di Spock durante questo articolo, con alcuni esempi pratici passo passo.

2. Dipendenza da Maven

Prima di iniziare, aggiungiamo le nostre dipendenze Maven:

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

Abbiamo aggiunto sia Spock che Groovy come faremmo con qualsiasi libreria standard. Tuttavia, poiché Groovy è un nuovo linguaggio JVM, dobbiamo includere il plug-in gmavenplus per poterlo compilare ed eseguire:

 org.codehaus.gmavenplus gmavenplus-plugin 1.5    compile testCompile    

Ora siamo pronti per scrivere il nostro primo test di Spock, che sarà scritto in codice Groovy. Si noti che stiamo usando Groovy e Spock solo per scopi di test ed è per questo che queste dipendenze hanno un ambito di test.

3. Struttura di un test di Spock

3.1. Specifiche e caratteristiche

Mentre stiamo scrivendo i nostri test in Groovy, dobbiamo aggiungerli alla directory src / test / groovy , invece che a src / test / java. Creiamo il nostro primo test in questa directory, chiamandolo Specification.groovy:

class FirstSpecification extends Specification { }

Tieni presente che stiamo estendendo l' interfaccia delle specifiche . Ogni classe Spock deve estenderla per rendere disponibile il framework. È così che ci consente di implementare la nostra prima funzionalità:

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

Prima di spiegare il codice, vale anche la pena notare che in Spock ciò a cui ci riferiamo come funzionalità è in qualche modo sinonimo di ciò che vediamo come un test in JUnit. Quindi, ogni volta che ci riferiamo a una funzionalità , in realtà ci riferiamo a un test.

Ora analizziamo la nostra caratteristica . In tal modo, dovremmo essere immediatamente in grado di vedere alcune differenze tra esso e Java.

La prima differenza è che il nome del metodo della funzionalità è scritto come una stringa normale. In JUnit, avremmo avuto un nome di metodo che utilizza camelcase o trattini bassi per separare le parole, il che non sarebbe stato così espressivo o leggibile dall'uomo.

Il prossimo è che il nostro codice di prova risiede in un blocco delle aspettative . Tratteremo i blocchi in modo più dettagliato a breve, ma essenzialmente sono un modo logico per suddividere i diversi passaggi dei nostri test.

Infine, ci rendiamo conto che non ci sono affermazioni. Questo perché l'asserzione è implicita, passa quando la nostra affermazione è vera e fallisce quando è falsa . Ancora una volta, tratteremo le asserzioni più in dettaglio a breve.

3.2. Blocchi

A volte, quando scriviamo un test su JUnit, potremmo notare che non esiste un modo espressivo per suddividerlo in parti. Ad esempio, se stessimo seguendo lo sviluppo guidato dal comportamento, potremmo finire per denotare il dato quando poi parti usando i commenti:

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

Spock affronta questo problema con i blocchi. I blocchi sono un modo nativo di Spock per suddividere le fasi del nostro test utilizzando le etichette. Ci danno etichette per dato quando allora e altro ancora:

  1. Setup (Alias ​​by Given) - Qui eseguiamo qualsiasi configurazione necessaria prima che venga eseguito un test. Questo è un blocco implicito, con il codice che non si trova in alcun blocco che ne diventa parte
  2. Quando : è qui che forniamo uno stimolo a ciò che è sotto test. In altre parole, dove invochiamo il nostro metodo in prova
  3. Allora - Questo è dove appartengono le affermazioni. In Spock, queste vengono valutate come semplici asserzioni booleane, che verranno trattate in seguito
  4. Aspettatevi : questo è un modo per eseguire il nostro stimolo e la nostra affermazione all'interno dello stesso blocco. A seconda di ciò che troviamo più espressivo, possiamo o meno scegliere di utilizzare questo blocco
  5. Pulizia : qui vengono eliminate tutte le risorse di dipendenza di test che altrimenti verrebbero lasciate indietro. Ad esempio, potremmo voler rimuovere qualsiasi file dal file system o rimuovere i dati di test scritti su un database

Proviamo di nuovo a implementare il nostro test, questa volta facendo pieno uso dei blocchi:

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

Come possiamo vedere, i blocchi aiutano il nostro test a diventare più leggibile.

3.3. Sfruttare le funzionalità Groovy per le asserzioni

All'interno delle poi e aspettano blocchi, affermazioni sono implicite .

Per lo più, ogni affermazione viene valutata e quindi fallisce se non è vera . Quando si accoppia questo con varie funzionalità di Groovy, fa un buon lavoro nel rimuovere la necessità di una libreria di asserzioni. Proviamo un'asserzione di elenco per dimostrarlo:

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

Anche se in questo articolo tocchiamo solo brevemente Groovy, vale la pena spiegare cosa sta succedendo qui.

Innanzitutto, Groovy ci offre modi più semplici per creare elenchi. Possiamo solo dichiarare i nostri elementi con parentesi quadre e internamente verrà istanziato un elenco .

In secondo luogo, poiché Groovy è dinamico, possiamo usare def che significa semplicemente che non stiamo dichiarando un tipo per le nostre variabili.

Infine, nel contesto della semplificazione del nostro test, la caratteristica più utile dimostrata è il sovraccarico degli operatori. Ciò significa che internamente, invece di effettuare un confronto dei riferimenti come in Java, verrà richiamato il metodo equals () per confrontare i due elenchi.

Vale anche la pena dimostrare cosa succede quando il nostro test fallisce. Facciamo in modo che si interrompa e quindi vediamo cosa viene inviato alla console:

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

Mentre tutto ciò che sta succedendo è chiamare equals () su due elenchi, Spock è abbastanza intelligente da eseguire un'analisi dettagliata dell'asserzione fallita, fornendoci informazioni utili per il debugging.

3.4. Affermare le eccezioni

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

Di nuovo, ci sono più possibilità, che potrebbero essere trattate in un futuro articolo più avanzato.

6. Conclusione

In questo articolo, abbiamo fornito una rapida sezione dei test con Spock.

Abbiamo dimostrato come, sfruttando Groovy, possiamo rendere i nostri test più espressivi rispetto al tipico stack JUnit. Abbiamo spiegato la struttura delle specifiche e delle caratteristiche .

E abbiamo mostrato quanto sia facile eseguire test basati sui dati, e anche quanto sia facile deridere e asserire tramite la funzionalità nativa di Spock.

L'implementazione di questi esempi può essere trovata su GitHub. Questo è un progetto basato su Maven, quindi dovrebbe essere facile da eseguire così com'è.