Utilizzo di Mockito per testare classi astratte


213

Mi piacerebbe testare una lezione astratta. Certo, posso scrivere manualmente un finto che eredita dalla classe.

Posso farlo usando un framework beffardo (sto usando Mockito) invece di fabbricare a mano il mio finto? Come?


2
A partire da Mockito 1.10.12 , Mockito supporta direttamente lo spionaggio / derisione di classi astratte:SomeAbstract spy = spy(SomeAbstract.class);
Pesche,

6
A partire da Mockito 2.7.14, puoi anche prendere in giro una classe astratta che richiede argomenti del costruttore tramitemock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS))
Gediminas Rimsa,

Risposte:


315

Il seguente suggerimento ti consente di testare le classi astratte senza creare una sottoclasse "reale" - il Mock è la sottoclasse.

usa Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS), quindi prendi in giro tutti i metodi astratti che vengono invocati.

Esempio:

public abstract class My {
  public Result methodUnderTest() { ... }
  protected abstract void methodIDontCareAbout();
}

public class MyTest {
    @Test
    public void shouldFailOnNullIdentifiers() {
        My my = Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS);
        Assert.assertSomething(my.methodUnderTest());
    }
}

Nota: il bello di questa soluzione è che non è necessario implementare i metodi astratti, purché non vengano mai invocati.

Secondo la mia onesta opinione, questo è più pulito rispetto all'utilizzo di una spia, poiché una spia richiede un'istanza, il che significa che devi creare una sottoclasse istantebile della tua classe astratta.


14
Come indicato di seguito, questo non funziona quando la classe astratta chiama metodi astratti per essere testata, come spesso accade.
Richard Nichols,

11
Questo effettivamente funziona quando la classe astratta chiama metodi astratti. Usa semplicemente la sintassi doReturn o doNothing invece di Mockito.quando esegui il mobbing dei metodi astratti, e se metti in stub le chiamate concrete, assicurati che il mobbing delle chiamate astratte venga prima di tutto.
Gonen I,

2
Come posso iniettare dipendenze in questo tipo di oggetto (classe astratta derisa che chiama metodi reali)?
Samuel,

2
Ciò si comporta in modo imprevisto se la classe in questione ha inizializzatori di istanza. Mockito salta gli inizializzatori per i mock, il che significa che le variabili di istanza che sono inizializzate in linea saranno inaspettatamente nulle, il che può causare NPE.
digitalbath,

1
Cosa succede se il costruttore della classe astratta accetta uno o più parametri?
SD

68

Se hai solo bisogno di testare alcuni dei metodi concreti senza toccare nessuno degli abstract, puoi usare CALLS_REAL_METHODS(vedi la risposta di Morten ), ma se il metodo concreto sotto test chiama alcuni degli abstract, o metodi di interfaccia non implementati, questo non funzionerà - Mockito si lamenterà "Impossibile chiamare il metodo reale sull'interfaccia java".

(Sì, è un design scadente, ma alcuni framework, ad esempio Tapestry 4, ti costringono a forza.)

La soluzione alternativa è quella di invertire questo approccio: utilizzare il normale comportamento simulato (ovvero, tutto è deriso / stub) e utilizzare doCallRealMethod()per richiamare esplicitamente il metodo concreto sotto test. Per esempio

public abstract class MyClass {
    @SomeDependencyInjectionOrSomething
    public abstract MyDependency getDependency();

    public void myMethod() {
        MyDependency dep = getDependency();
        dep.doSomething();
    }
}

public class MyClassTest {
    @Test
    public void myMethodDoesSomethingWithDependency() {
        MyDependency theDependency = mock(MyDependency.class);

        MyClass myInstance = mock(MyClass.class);

        // can't do this with CALLS_REAL_METHODS
        when(myInstance.getDependency()).thenReturn(theDependency);

        doCallRealMethod().when(myInstance).myMethod();
        myInstance.myMethod();

        verify(theDependency, times(1)).doSomething();
    }
}

Aggiornato per aggiungere:

Per i metodi non nulli, dovrai utilizzare thenCallRealMethod()invece, ad esempio:

when(myInstance.myNonVoidMethod(someArgument)).thenCallRealMethod();

Altrimenti Mockito si lamenterà "Rilevamento di mozziconi incompiuti".


9
Ciò funzionerà in alcuni casi, tuttavia Mockito non chiama il costruttore della classe astratta sottostante con questo metodo. Ciò può causare il fallimento del "metodo reale" a causa della creazione di uno scenario imprevisto. Pertanto, questo metodo non funzionerà in tutti i casi.
Richard Nichols,

3
Sì, non puoi contare sullo stato dell'oggetto, solo il codice nel metodo chiamato.
David Moles,

Oh, quindi i metodi degli oggetti vengono separati dallo stato, fantastico.
haelix,

17

Puoi farlo usando una spia (usa l'ultima versione di Mockito 1.8+ però).

public abstract class MyAbstract {
  public String concrete() {
    return abstractMethod();
  }
  public abstract String abstractMethod();
}

public class MyAbstractImpl extends MyAbstract {
  public String abstractMethod() {
    return null;
  }
}

// your test code below

MyAbstractImpl abstractImpl = spy(new MyAbstractImpl());
doReturn("Blah").when(abstractImpl).abstractMethod();
assertTrue("Blah".equals(abstractImpl.concrete()));

14

I framework di derisione sono progettati per semplificare la derisione delle dipendenze della classe che si sta testando. Quando si utilizza un framework beffardo per deridere una classe, la maggior parte dei framework crea dinamicamente una sottoclasse e sostituisce l'implementazione del metodo con il codice per rilevare quando viene chiamato un metodo e restituire un valore falso.

Quando si testa una classe astratta, si desidera eseguire i metodi non astratti del Soggetto Sotto Test (SUT), quindi un framework beffardo non è quello che si desidera.

Parte della confusione è che la risposta alla domanda a cui ti sei collegato ha detto di fabbricare a mano un finto che si estende dalla tua classe astratta. Non definirei una tale classe una derisione. Una simulazione è una classe che viene utilizzata in sostituzione di una dipendenza, è programmata con aspettative e può essere interrogata per vedere se tali aspettative sono soddisfatte.

Invece, suggerisco di definire una sottoclasse non astratta della tua classe astratta nel tuo test. Se questo risulta in troppo codice, allora potrebbe essere un segno che la tua classe è difficile da estendere.

Una soluzione alternativa sarebbe quella di rendere astratto il tuo caso di test, con un metodo astratto per creare il SUT (in altre parole, il caso di test userebbe il modello di progettazione Metodo del modello).


8

Prova a utilizzare una risposta personalizzata.

Per esempio:

import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class CustomAnswer implements Answer<Object> {

    public Object answer(InvocationOnMock invocation) throws Throwable {

        Answer<Object> answer = null;

        if (isAbstract(invocation.getMethod().getModifiers())) {

            answer = Mockito.RETURNS_DEFAULTS;

        } else {

            answer = Mockito.CALLS_REAL_METHODS;
        }

        return answer.answer(invocation);
    }
}

Restituirà la derisione per metodi astratti e chiamerà il metodo reale per metodi concreti.


5

Ciò che mi fa sentire davvero male nel deridere le classi astratte è il fatto che né il costruttore predefinito YourAbstractClass () viene chiamato (mancante super () in mock) né sembra che ci sia un modo in Mockito di inizializzare le proprietà mock predefinite (es. Proprietà List con ArrayList o LinkedList vuoti).

La mia classe astratta (in pratica viene generato il codice sorgente della classe) NON fornisce un'iniezione del setter di dipendenze per gli elementi dell'elenco, né un costruttore in cui inizializza gli elementi dell'elenco (che ho provato ad aggiungere manualmente).

Solo gli attributi di classe utilizzano l'inizializzazione predefinita: Elenco privato dep1 = new ArrayList; Elenco privato dep2 = new ArrayList

Quindi non c'è modo di deridere una classe astratta senza usare un'implementazione di oggetto reale (ad esempio definizione di classe interna in classe di test unit, metodi astratti di sostituzione) e spionaggio dell'oggetto reale (che esegue l'inizializzazione del campo corretta).

Peccato che solo PowerMock possa aiutare ulteriormente qui.


2

Supponendo che le tue classi di test siano nello stesso pacchetto (sotto una radice di origine diversa) delle tue classi sotto test puoi semplicemente creare il mock:

YourClass yourObject = mock(YourClass.class);

e chiama i metodi che desideri testare come faresti con qualsiasi altro metodo.

Devi fornire aspettative per ogni metodo chiamato con l'aspettativa su qualsiasi metodo concreto che chiama il metodo super - non sei sicuro di come lo faresti con Mockito, ma credo che sia possibile con EasyMock.

Tutto ciò che sta facendo è creare un'istanza concreta di YouClasse risparmiarti lo sforzo di fornire implementazioni vuote di ciascun metodo astratto.

A parte questo, trovo spesso utile implementare la classe astratta nel mio test, dove funge da implementazione di esempio che collaudo attraverso la sua interfaccia pubblica, sebbene ciò dipenda dalla funzionalità fornita dalla classe astratta.


3
Ma usare la simulazione non metterà alla prova i metodi concreti di YourClass o sbaglio? Questo non è quello che cerco.
ripper234,

1
È corretto, quanto sopra non funzionerà se si desidera invocare i metodi concreti sulla classe astratta.
Richard Nichols,

Mi scuso, modificherò il bit sull'aspettativa, che sono richiesti per ogni metodo che chiami non solo quelli astratti.
Nick Holt,

ma poi stai ancora testando il tuo finto, non i metodi concreti.
Jonatan Cloutier,

2

È possibile estendere la classe astratta con una classe anonima nel test. Ad esempio (utilizzando Junit 4):

private AbstractClassName classToTest;

@Before
public void preTestSetup()
{
    classToTest = new AbstractClassName() { };
}

// Test the AbstractClassName methods.

2

Mockito consente di deridere classi astratte mediante l' @Mockannotazione:

public abstract class My {

    public abstract boolean myAbstractMethod();

    public void myNonAbstractMethod() {
        // ...
    }
}

@RunWith(MockitoJUnitRunner.class)
public class MyTest {

    @Mock(answer = Answers.CALLS_REAL_METHODS)
    private My my;

    @Test
    private void shouldPass() {
        BDDMockito.given(my.myAbstractMethod()).willReturn(true);
        my.myNonAbstractMethod();
        // ...
    }
}

Lo svantaggio è che non può essere utilizzato se sono necessari parametri del costruttore.


0

Puoi creare un'istanza di una classe anonima, iniettare le tue beffe e quindi testare quella classe.

@RunWith(MockitoJUnitRunner.class)
public class ClassUnderTest_Test {

    private ClassUnderTest classUnderTest;

    @Mock
    MyDependencyService myDependencyService;

    @Before
    public void setUp() throws Exception {
        this.classUnderTest = getInstance();
    }

    private ClassUnderTest getInstance() {
        return new ClassUnderTest() {

            private ClassUnderTest init(
                    MyDependencyService myDependencyService
            ) {
                this.myDependencyService = myDependencyService;
                return this;
            }

            @Override
            protected void myMethodToTest() {
                return super.myMethodToTest();
            }
        }.init(myDependencyService);
    }
}

Tieni presente che la visibilità deve essere protectedper la proprietà myDependencyServicedella classe astratta ClassUnderTest.


0

In Whitebox.invokeMethod(..)questo caso i PowerMock possono essere utili.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.