Una guida alle aspettative di JMockit

1. Introduzione

Questo articolo è la seconda puntata della serie JMockit. Potresti leggere il primo articolo poiché presumiamo che tu abbia già familiarità con le basi di JMockit.

Oggi andremo più a fondo e ci concentreremo sulle aspettative. Mostreremo come definire una corrispondenza di argomenti più specifica o generica e modi più avanzati di definire i valori.

2. Corrispondenza dei valori degli argomenti

I seguenti approcci si applicano sia alle aspettative che alle verifiche .

2.1. Campi "Qualsiasi"

JMockit offre una serie di campi di utilità per rendere la corrispondenza degli argomenti più generica. Una di queste utilità sono i campi anyX .

Questi controlleranno che sia stato passato qualsiasi valore e che ce ne sia uno per ogni tipo primitivo (e la classe wrapper corrispondente), uno per le stringhe e uno "universale" di tipo Object .

Vediamo un esempio:

public interface ExpectationsCollaborator { String methodForAny1(String s, int i, Boolean b); void methodForAny2(Long l, List lst); } @Test public void test(@Mocked ExpectationsCollaborator mock) throws Exception { new Expectations() {{ mock.methodForAny1(anyString, anyInt, anyBoolean); result = "any"; }}; Assert.assertEquals("any", mock.methodForAny1("barfooxyz", 0, Boolean.FALSE)); mock.methodForAny2(2L, new ArrayList()); new FullVerifications() {{ mock.methodForAny2(anyLong, (List) any); }}; }

È necessario tenere presente che quando si utilizza il campo any , è necessario trasmetterlo al tipo previsto. L'elenco completo dei campi è presente nella documentazione.

2.2. Metodi "con"

JMockit fornisce anche diversi metodi per aiutare con la corrispondenza di argomenti generici. Questi sono i metodi withX .

Questi consentono una corrispondenza un po 'più avanzata rispetto ai campi anyX . Possiamo vedere un esempio qui in cui definiremo un'aspettativa per un metodo che verrà attivato con una stringa contenente foo , un numero intero diverso da 1, un booleano non nullo e qualsiasi istanza della classe List :

public interface ExpectationsCollaborator { String methodForWith1(String s, int i); void methodForWith2(Boolean b, List l); } @Test public void testForWith(@Mocked ExpectationsCollaborator mock) throws Exception { new Expectations() {{ mock.methodForWith1(withSubstring("foo"), withNotEqual(1)); result = "with"; }}; assertEquals("with", mock.methodForWith1("barfooxyz", 2)); mock.methodForWith2(Boolean.TRUE, new ArrayList()); new Verifications() {{ mock.methodForWith2(withNotNull(), withInstanceOf(List.class)); }}; }

È possibile vedere la lista completa dei withX metodi sulla documentazione di JMockit.

Tieni presente che lo speciale con (Delegato) e withArgThat (Matcher) sarà trattato nella propria sottosezione.

2.3. Null non è nullo

Qualcosa che è bene capire prima o poi è che null non viene utilizzato per definire un argomento per il quale null è stato passato a un mock.

In realtà, null viene utilizzato come zucchero sintattico per definire che qualsiasi oggetto verrà passato (quindi può essere utilizzato solo per parametri di tipo riferimento). Per verificare specificamente che un dato parametro riceva il riferimento null , è possibile utilizzare il matcher withNull () .

Per il prossimo esempio, definiremo il comportamento per un mock, che dovrebbe essere attivato quando gli argomenti passati sono: any string, any List e un riferimento null :

public interface ExpectationsCollaborator { String methodForNulls1(String s, List l); void methodForNulls2(String s, List l); } @Test public void testWithNulls(@Mocked ExpectationsCollaborator mock){ new Expectations() {{ mock.methodForNulls1(anyString, null); result = "null"; }}; assertEquals("null", mock.methodForNulls1("blablabla", new ArrayList())); mock.methodForNulls2("blablabla", null); new Verifications() {{ mock.methodForNulls2(anyString, (List) withNull()); }}; }

Notare la differenza: null significa qualsiasi lista e withNull () indica un riferimento nullo a una lista. In particolare, questo evita la necessità di eseguire il cast del valore sul tipo di parametro dichiarato (vedere che è stato necessario eseguire il cast del terzo argomento ma non del secondo).

L'unica condizione per poterlo utilizzare è che almeno un abbinamento di argomenti espliciti sia stato utilizzato per l'aspettativa (un metodo with o un campo qualsiasi ).

2.4. Campo "Times"

A volte, vogliamo limitare il numero di invocazioni previste per un metodo deriso. Per questo, JMockit ha le parole riservate times , minTimes e maxTimes (tutti e tre consentono solo numeri interi non negativi).

public interface ExpectationsCollaborator { void methodForTimes1(); void methodForTimes2(); void methodForTimes3(); } @Test public void testWithTimes(@Mocked ExpectationsCollaborator mock) { new Expectations() {{ mock.methodForTimes1(); times = 2; mock.methodForTimes2(); }}; mock.methodForTimes1(); mock.methodForTimes1(); mock.methodForTimes2(); mock.methodForTimes3(); mock.methodForTimes3(); mock.methodForTimes3(); new Verifications() {{ mock.methodForTimes3(); minTimes = 1; maxTimes = 3; }}; }

In questo esempio, abbiamo definito che esattamente due invocazioni (non una, non tre, esattamente due) di methodForTimes1 () dovrebbero essere eseguite utilizzando la riga times = 2; .

Quindi abbiamo utilizzato il comportamento predefinito (se non viene fornito alcun vincolo di ripetizione minTimes = 1; viene utilizzato) per definire che verrà eseguita almeno una chiamata a methodForTimes2 ().

Infine, utilizzando minTimes = 1; seguito da maxTimes = 3; abbiamo definito che si sarebbero verificate da una a tre invocazioni a methodForTimes3 () .

Tieni presente che sia minTimes che maxTimes possono essere specificati per la stessa aspettativa, a condizione che minTimes sia assegnato per primo. D'altra parte, i tempi possono essere usati solo da soli.

2.5. Corrispondenza argomenti personalizzata

A volte la corrispondenza degli argomenti non è così diretta come la semplice specifica di un valore o l'utilizzo di alcune delle utilità predefinite ( anyX o withX ).

In questi casi, JMockit si affida all'interfaccia Matcher di Hamcrest . Hai solo bisogno di definire un matcher per lo scenario di test specifico e utilizzare quel matcher con una chiamata withArgThat () .

Vediamo un esempio per abbinare una classe specifica a un oggetto passato:

public interface ExpectationsCollaborator { void methodForArgThat(Object o); } public class Model { public String getInfo(){ return "info"; } } @Test public void testCustomArgumentMatching(@Mocked ExpectationsCollaborator mock) { new Expectations() {{ mock.methodForArgThat(withArgThat(new BaseMatcher() { @Override public boolean matches(Object item) { return item instanceof Model && "info".equals(((Model) item).getInfo()); } @Override public void describeTo(Description description) { } })); }}; mock.methodForArgThat(new Model()); }

3. Restituzione di valori

Vediamo ora i valori di ritorno; tieni presente che i seguenti approcci si applicano solo alle aspettative poiché non è possibile definire valori di ritorno per le verifiche .

3.1. Risultato e ritorni (...)

Quando si utilizza JMockit, sono disponibili tre diversi modi per definire il risultato atteso dell'invocazione di un metodo fittizio. Di tutti e tre, parleremo ora dei primi due (i più semplici) che copriranno sicuramente il 90% dei casi d'uso quotidiani.

These two are the result field and the returns(Object…) method:

  • With the result field, you can define one return value for any non-void returning mocked method. This return value can also be an exception to be thrown (this time working for both non-void and void returning methods).
    • Several result field assignations can be done in order to return more than one value for more than one method invocations (you can mix both return values and errors to be thrown).
    • The same behaviour will be achieved when assigning to result a list or an array of values (of the same type than the return type of the mocked method, NO exceptions here).
  • The returns(Object…) method is syntactic sugar for returning several values of the same time.

This is more easily shown with a code snippet:

public interface ExpectationsCollaborator{ String methodReturnsString(); int methodReturnsInt(); } @Test public void testResultAndReturns(@Mocked ExpectationsCollaborator mock) { new Expectations() {{ mock.methodReturnsString(); result = "foo"; result = new Exception(); result = "bar"; returns("foo", "bar"); mock.methodReturnsInt(); result = new int[]{1, 2, 3}; result = 1; }}; assertEquals("Should return foo", "foo", mock.methodReturnsString()); try { mock.methodReturnsString(); fail("Shouldn't reach here"); } catch (Exception e) { // NOOP } assertEquals("Should return bar", "bar", mock.methodReturnsString()); assertEquals("Should return 1", 1, mock.methodReturnsInt()); assertEquals("Should return 2", 2, mock.methodReturnsInt()); assertEquals("Should return 3", 3, mock.methodReturnsInt()); assertEquals("Should return foo", "foo", mock.methodReturnsString()); assertEquals("Should return bar", "bar", mock.methodReturnsString()); assertEquals("Should return 1", 1, mock.methodReturnsInt()); }

In this example, we have defined that for the first three calls to methodReturnsString() the expected returns are (in order) “foo”, an exception and “bar”. We achieved this using three different assignations to the result field.

Then on line 14, we defined that for the fourth and fifth calls, “foo” and “bar” should be returned using the returns(Object…) method.

For the methodReturnsInt() we defined on line 13 to return 1, 2 and lastly 3 by assigning an array with the different results to the result field and on line 15 we defined to return 1 by a simple assignation to the result field.

As you can see there are several ways of defining return values for mocked methods.

3.2. Delegators

To end the article we're going to cover the third way of defining the return value: the Delegate interface. This interface is used for defining more complex return values when defining mocked methods.

We're going to see an example to simply the explaining:

public interface ExpectationsCollaborator { int methodForDelegate(int i); } @Test public void testDelegate(@Mocked ExpectationsCollaborator mock) { new Expectations() {{ mock.methodForDelegate(anyInt); result = new Delegate() { int delegate(int i) throws Exception { if (i < 3) { return 5; } else { throw new Exception(); } } }; }}; assertEquals("Should return 5", 5, mock.methodForDelegate(1)); try { mock.methodForDelegate(3); fail("Shouldn't reach here"); } catch (Exception e) { } } 

The way to use a delegator is to create a new instance for it and assign it to a returns field. In this new instance, you should create a new method with the same parameters and return type than the mocked method (you can use any name for it). Inside this new method, use whatever implementation you want in order to return the desired value.

In the example, we did an implementation in which 5 should be returned when the value passed to the mocked method is less than 3 and an exception is thrown otherwise (note that we had to use times = 2; so that the second invocation is expected as we lost the default behaviour by defining a return value).

Può sembrare un bel po 'di codice, ma in alcuni casi sarà l'unico modo per ottenere il risultato che vogliamo.

4. Conclusione

Con questo, abbiamo praticamente mostrato tutto ciò di cui abbiamo bisogno per creare aspettative e verifiche per i nostri test quotidiani.

Ovviamente pubblicheremo più articoli su JMockit, quindi resta sintonizzato per saperne di più.

E, come sempre, l'implementazione completa di questo tutorial può essere trovata nel progetto GitHub.

4.1. Articoli della serie

Tutti gli articoli della serie:

  • JMockit 101
  • Una guida alle aspettative di JMockit
  • Utilizzo avanzato di JMockit