Utilizzo avanzato di JMockit

1. Introduzione

In questo articolo andremo oltre le basi di JMockit e inizieremo a esaminare alcuni scenari avanzati, come:

  • Faking (o l' API MockUp )
  • La classe di utilità deincapsulamento
  • Come deridere più di un'interfaccia usando un solo mock
  • Come riutilizzare aspettative e verifiche

Se vuoi scoprire le basi di JMockit, controlla altri articoli di questa serie. Puoi trovare link rilevanti in fondo alla pagina.

2. Dipendenza da Maven

Innanzitutto, dobbiamo aggiungere la dipendenza jmockit al nostro progetto:

 org.jmockit jmockit 1.41  

Successivamente, continueremo con gli esempi.

3. Metodi privati ​​/ derisione delle classi interne

Deridere e testare metodi privati ​​o classi interne spesso non è considerata una buona pratica.

Il ragionamento alla base è che se sono privati, non dovrebbero essere testati direttamente in quanto sono le viscere più interne della classe, ma a volte deve ancora essere fatto, specialmente quando si tratta di codice legacy.

Con JMockit, hai due opzioni per gestirli:

  • L' API MockUp per modificare la reale implementazione (per il secondo caso)
  • La classe di utilità Deencapsulation , per chiamare direttamente qualsiasi metodo (per il primo caso)

Tutti i seguenti esempi verranno eseguiti per la seguente classe e supporremo che vengano eseguiti su una classe di test con la stessa configurazione della prima (per evitare di ripetere il codice):

public class AdvancedCollaborator { int i; private int privateField = 5; // default constructor omitted public AdvancedCollaborator(String string) throws Exception{ i = string.length(); } public String methodThatCallsPrivateMethod(int i) { return privateMethod() + i; } public int methodThatReturnsThePrivateField() { return privateField; } private String privateMethod() { return "default:"; } class InnerAdvancedCollaborator {...} }

3.1. Fingere con MockUp

L'API Mockup di JMockit fornisce supporto per la creazione di false implementazioni o mock-up . In genere, un mock-up prende di mira alcuni metodi e / o costruttori nella classe da falsificare, lasciando la maggior parte degli altri metodi e costruttori non modificati. Ciò consente una riscrittura completa di una classe, quindi qualsiasi metodo o costruttore (con qualsiasi modificatore di accesso) può essere mirato.

Vediamo come possiamo ridefinire privateMethod () utilizzando l'API di Mockup :

@RunWith(JMockit.class) public class AdvancedCollaboratorTest { @Tested private AdvancedCollaborator mock; @Test public void testToMockUpPrivateMethod() { new MockUp() { @Mock private String privateMethod() { return "mocked: "; } }; String res = mock.methodThatCallsPrivateMethod(1); assertEquals("mocked: 1", res); } }

In questo esempio stiamo definendo un nuovo MockUp per la classe AdvancedCollaborator utilizzando l' annotazione @Mock su un metodo con firma corrispondente. Dopodiché, le chiamate a quel metodo verranno delegate al nostro mock.

Possiamo anche usarlo per simulare il costruttore di una classe che necessita di argomenti o configurazione specifici per semplificare i test:

@Test public void testToMockUpDifficultConstructor() throws Exception{ new MockUp() { @Mock public void $init(Invocation invocation, String string) { ((AdvancedCollaborator)invocation.getInvokedInstance()).i = 1; } }; AdvancedCollaborator coll = new AdvancedCollaborator(null); assertEquals(1, coll.i); }

In questo esempio, possiamo vedere che per il mocking del costruttore è necessario deridere il metodo $ init . È possibile passare un argomento aggiuntivo di tipo Invocation, con il quale è possibile accedere alle informazioni sull'invocazione del metodo deriso, inclusa l'istanza su cui viene eseguita l'invocazione.

3.2. Utilizzo della classe di deincapsulazione

JMockit include una classe di utilità di test: Deencapsulation . Come indica il nome, viene utilizzato per de-incapsulare uno stato di un oggetto e, utilizzandolo, è possibile semplificare il test accedendo a campi e metodi a cui non sarebbe possibile accedere altrimenti.

Puoi invocare un metodo:

@Test public void testToCallPrivateMethodsDirectly(){ Object value = Deencapsulation.invoke(mock, "privateMethod"); assertEquals("default:", value); }

Puoi anche impostare i campi:

@Test public void testToSetPrivateFieldDirectly(){ Deencapsulation.setField(mock, "privateField", 10); assertEquals(10, mock.methodThatReturnsThePrivateField()); }

E ottieni campi:

@Test public void testToGetPrivateFieldDirectly(){ int value = Deencapsulation.getField(mock, "privateField"); assertEquals(5, value); }

E crea nuove istanze di classi:

@Test public void testToCreateNewInstanceDirectly(){ AdvancedCollaborator coll = Deencapsulation .newInstance(AdvancedCollaborator.class, "foo"); assertEquals(3, coll.i); }

Anche nuove istanze di classi interne:

@Test public void testToCreateNewInnerClassInstanceDirectly(){ InnerCollaborator inner = Deencapsulation .newInnerInstance(InnerCollaborator.class, mock); assertNotNull(inner); }

Come puoi vedere, la classe Deincapsulamento è estremamente utile quando si testano classi a tenuta d'aria. Un esempio potrebbe essere quello di impostare le dipendenze di una classe che utilizza annotazioni @Autowired su campi privati ​​e non ha setter per loro, o di testare le classi interne senza dover dipendere dall'interfaccia pubblica della sua classe contenitore.

4. Deridere più interfacce in una stessa simulazione

Supponiamo che tu voglia testare una classe - non ancora implementata - ma sai per certo che implementerà diverse interfacce.

Di solito, non saresti in grado di testare detta classe prima di implementarla, ma con JMockit hai la possibilità di preparare i test in anticipo deridendo più di un'interfaccia usando un oggetto fittizio.

Ciò può essere ottenuto utilizzando generics e definendo un tipo che estenda diverse interfacce. Questo tipo generico può essere definito per un'intera classe di test o per un solo metodo di test.

Ad esempio, creeremo un mock per le interfacce List e Comparable in due modi :

@RunWith(JMockit.class) public class AdvancedCollaboratorTest
    
     > { @Mocked private MultiMock multiMock; @Test public void testOnClass() { new Expectations() {{ multiMock.get(5); result = "foo"; multiMock.compareTo((List) any); result = 0; }}; assertEquals("foo", multiMock.get(5)); assertEquals(0, multiMock.compareTo(new ArrayList())); } @Test public 
     
      > void testOnMethod(@Mocked M mock) { new Expectations() {{ mock.get(5); result = "foo"; mock.compareTo((List) any); result = 0; }}; assertEquals("foo", mock.get(5)); assertEquals(0, mock.compareTo(new ArrayList())); } }
     
    

Come puoi vedere nella riga 2, possiamo definire un nuovo tipo di test per l'intero test utilizzando generics sul nome della classe. In questo modo, MultiMock sarà disponibile come tipo e sarai in grado di creare mock per esso utilizzando una qualsiasi delle annotazioni di JMockit.

Nelle righe dalla 7 alla 18, possiamo vedere un esempio che utilizza un mock di una multi-classe definita per l'intera classe di test.

Se hai bisogno del mock multi-interfaccia per un solo test, puoi ottenere ciò definendo il tipo generico sulla firma del metodo e passando un nuovo mock di quel nuovo generico come argomento del metodo di test. Nelle righe da 20 a 32, possiamo vedere un esempio di ciò per lo stesso comportamento testato del test precedente.

5. Riutilizzo di aspettative e verifiche

Alla fine, durante il test delle classi, potresti incontrare casi in cui stai ripetendo le stesse aspettative e / o verifiche più e più volte. Per facilitare ciò, puoi riutilizzarli entrambi facilmente.

Lo spiegheremo con un esempio (stiamo usando le classi Model, Collaborator e Performer dal nostro articolo JMockit 101):

@RunWith(JMockit.class) public class ReusingTest { @Injectable private Collaborator collaborator; @Mocked private Model model; @Tested private Performer performer; @Before public void setup(){ new Expectations(){{ model.getInfo(); result = "foo"; minTimes = 0; collaborator.collaborate("foo"); result = true; minTimes = 0; }}; } @Test public void testWithSetup() { performer.perform(model); verifyTrueCalls(1); } protected void verifyTrueCalls(int calls){ new Verifications(){{ collaborator.receive(true); times = calls; }}; } final class TrueCallsVerification extends Verifications{ public TrueCallsVerification(int calls){ collaborator.receive(true); times = calls; } } @Test public void testWithFinalClass() { performer.perform(model); new TrueCallsVerification(1); } }

In questo esempio, puoi vedere nelle righe da 15 a 18 che stiamo preparando un'aspettativa per ogni test in modo che model.getInfo () restituisca sempre "foo" e che collaborator.collaborate () si aspetti sempre "foo" come argomento e restituendo true . Mettiamo l' istruzione minTimes = 0 in modo che non vengano visualizzati errori quando non vengono effettivamente utilizzati nei test.

Inoltre, abbiamo creato il metodo verifyTrueCalls (int) per semplificare le verifiche al metodo collaborator.receive (boolean) quando l'argomento passato è vero .

Infine, puoi anche creare nuovi tipi di aspettative e verifiche specifiche semplicemente estendendo qualsiasi classe di aspettative o verifiche . Quindi si definisce un costruttore se è necessario configurare il comportamento e creare una nuova istanza di detto tipo in un test come facciamo nelle righe da 33 a 43.

6. Conclusione

Con questa puntata della serie JMockit, abbiamo toccato diversi argomenti avanzati che ti aiuteranno sicuramente con derisioni e test quotidiani.

Potremmo fare più articoli su JMockit, quindi resta sintonizzato per saperne di più.

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

6.1. Articoli della serie

Tutti gli articoli della serie:

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