Testare una classe astratta con JUnit

1. Panoramica

In questo tutorial, analizzeremo vari casi d'uso e possibili soluzioni alternative allo unit test di classi astratte con metodi non astratti.

Nota che il test delle classi astratte dovrebbe quasi sempre passare attraverso l'API pubblica delle implementazioni concrete , quindi non applicare le tecniche seguenti a meno che tu non sia sicuro di cosa stai facendo.

2. Dipendenze di Maven

Cominciamo con le dipendenze di Maven:

 org.junit.jupiter junit-jupiter-engine 5.1.0 test   org.mockito mockito-core 2.8.9 test   org.powermock powermock-module-junit4 1.7.4 test   junit junit     org.powermock powermock-api-mockito2 1.7.4 test 

Puoi trovare le ultime versioni di queste librerie su Maven Central.

Powermock non è completamente supportato per Junit5. Inoltre, powermock-module-junit4 viene utilizzato solo per un esempio presentato nella sezione 5.

3. Metodo indipendente non astratto

Consideriamo un caso in cui abbiamo una classe astratta con un metodo pubblico non astratto:

public abstract class AbstractIndependent { public abstract int abstractFunc(); public String defaultImpl() { return "DEFAULT-1"; } }

Vogliamo testare il metodo defaultImpl () e abbiamo due possibili soluzioni: usare una classe concreta o usare Mockito.

3.1. Utilizzando una classe concreta

Crea una classe concreta che estende la classe AbstractIndependent e usala per testare il metodo:

public class ConcreteImpl extends AbstractIndependent { @Override public int abstractFunc() { return 4; } }
@Test public void givenNonAbstractMethod_whenConcreteImpl_testCorrectBehaviour() { ConcreteImpl conClass = new ConcreteImpl(); String actual = conClass.defaultImpl(); assertEquals("DEFAULT-1", actual); }

Lo svantaggio di questa soluzione è la necessità di creare la classe concreta con implementazioni fittizie di tutti i metodi astratti.

3.2. Utilizzando Mockito

In alternativa, possiamo usare Mockito per creare un mock:

@Test public void givenNonAbstractMethod_whenMockitoMock_testCorrectBehaviour() { AbstractIndependent absCls = Mockito.mock( AbstractIndependent.class, Mockito.CALLS_REAL_METHODS); assertEquals("DEFAULT-1", absCls.defaultImpl()); }

La parte più importante qui è la preparazione del mock per utilizzare il codice reale quando un metodo viene invocato utilizzando Mockito.CALLS_REAL_METHODS .

4. Metodo astratto chiamato da metodo non astratto

In questo caso, il metodo non astratto definisce il flusso di esecuzione globale, mentre il metodo astratto può essere scritto in modi diversi a seconda del caso d'uso:

public abstract class AbstractMethodCalling { public abstract String abstractFunc(); public String defaultImpl() { String res = abstractFunc(); return (res == null) ? "Default" : (res + " Default"); } }

Per testare questo codice, possiamo utilizzare gli stessi due approcci di prima: creare una classe concreta o utilizzare Mockito per creare un mock:

@Test public void givenDefaultImpl_whenMockAbstractFunc_thenExpectedBehaviour() { AbstractMethodCalling cls = Mockito.mock(AbstractMethodCalling.class); Mockito.when(cls.abstractFunc()) .thenReturn("Abstract"); Mockito.doCallRealMethod() .when(cls) .defaultImpl(); assertEquals("Abstract Default", cls.defaultImpl()); }

Qui, abstractFunc () è stub con il valore di ritorno che preferiamo per il test. Ciò significa che quando chiamiamo il metodo non astratto defaultImpl () , utilizzerà questo stub.

5. Metodo non astratto con ostruzione del test

In alcuni scenari, il metodo che vogliamo testare chiama un metodo privato che contiene un'ostruzione del test.

Dobbiamo bypassare il metodo di test che ostruisce prima di testare il metodo target:

public abstract class AbstractPrivateMethods { public abstract int abstractFunc(); public String defaultImpl() { return getCurrentDateTime() + "DEFAULT-1"; } private String getCurrentDateTime() { return LocalDateTime.now().toString(); } }

In questo esempio, il metodo defaultImpl () chiama il metodo privato getCurrentDateTime () . Questo metodo privato ottiene l'ora corrente in fase di esecuzione, che dovrebbe essere evitata nei nostri unit test.

Ora, per deridere il comportamento standard di questo metodo privato, non possiamo nemmeno usare Mockito perché non può controllare i metodi privati.

Invece, abbiamo bisogno di usare PowerMock ( n ote che questo esempio funziona solo con JUnit 4 perché il supporto per questa dipendenza non è disponibile per JUnit 5 ):

@RunWith(PowerMockRunner.class) @PrepareForTest(AbstractPrivateMethods.class) public class AbstractPrivateMethodsUnitTest { @Test public void whenMockPrivateMethod_thenVerifyBehaviour() { AbstractPrivateMethods mockClass = PowerMockito.mock(AbstractPrivateMethods.class); PowerMockito.doCallRealMethod() .when(mockClass) .defaultImpl(); String dateTime = LocalDateTime.now().toString(); PowerMockito.doReturn(dateTime).when(mockClass, "getCurrentDateTime"); String actual = mockClass.defaultImpl(); assertEquals(dateTime + "DEFAULT-1", actual); } }

Bit importanti in questo esempio:

  • @RunWith definisce PowerMock come il corridore per il test
  • @PrepareForTest (class) dice a PowerMock di preparare la classe per l'elaborazione successiva

È interessante notare che stiamo chiedendo a PowerMock di bloccare il metodo privato getCurrentDateTime (). PowerMock utilizzerà la riflessione per trovarlo perché non è accessibile dall'esterno.

Quindi , quando chiamiamo defaultImpl () , verrà richiamato lo stub creato per un metodo privato invece del metodo effettivo.

6. Metodo non astratto che accede ai campi dell'istanza

Le classi astratte possono avere uno stato interno implementato con i campi di classe. Il valore dei campi potrebbe avere un effetto significativo sul metodo da testare.

Se un campo è pubblico o protetto, possiamo accedervi facilmente dal metodo di test.

Ma se è privato, dobbiamo usare PowerMockito :

public abstract class AbstractInstanceFields { protected int count; private boolean active = false; public abstract int abstractFunc(); public String testFunc() { if (count > 5) { return "Overflow"; } return active ? "Added" : "Blocked"; } }

Here, the testFunc() method is using instance-level fields count and active before it returns.

When testing testFunc(), we can change the value of the count field by accessing instance created using Mockito.

On the other hand, to test the behavior with the private active field, we'll again have to use PowerMockito, and its Whitebox class:

@Test public void whenPowerMockitoAndActiveFieldTrue_thenCorrectBehaviour() { AbstractInstanceFields instClass = PowerMockito.mock(AbstractInstanceFields.class); PowerMockito.doCallRealMethod() .when(instClass) .testFunc(); Whitebox.setInternalState(instClass, "active", true); assertEquals("Added", instClass.testFunc()); }

We're creating a stub class using PowerMockito.mock(), and we're using Whitebox class to control object's internal state.

The value of the active field is changed to true.

7. Conclusione

In questo tutorial, abbiamo visto più esempi che coprono molti casi d'uso. Possiamo usare classi astratte in molti più scenari a seconda del progetto seguito.

Inoltre, la scrittura di unit test per metodi di classe astratti è importante quanto per classi e metodi normali. Possiamo testarli ciascuno utilizzando tecniche diverse o diverse librerie di supporto per i test disponibili.

Il codice sorgente completo è disponibile su GitHub.