Deridere le variabili membro di una classe usando Mockito


136

Sono un novizio dello sviluppo e in particolare dei test unitari. Immagino che il mio requisito sia piuttosto semplice, ma sono curioso di conoscere i pensieri degli altri su questo.

Supponiamo che io abbia due classi così:

public class First {

    Second second ;

    public First(){
        second = new Second();
    }

    public String doSecond(){
        return second.doSecond();
    }
}

class Second {

    public String doSecond(){
        return "Do Something";
    }
}

Diciamo che sto scrivendo unit test per testare il First.doSecond()metodo. Tuttavia, supponiamo, voglio deridere la Second.doSecond()classe in questo modo. Sto usando Mockito per fare questo.

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Vedo che la derisione non ha effetto e l'asserzione fallisce. Non c'è modo di deridere le variabili membro di una classe che voglio testare. ?

Risposte:


86

È necessario fornire un modo per accedere alle variabili membro in modo da poter passare in modo fittizio (i modi più comuni sarebbero un metodo setter o un costruttore che accetta un parametro).

Se il codice non fornisce un modo per farlo, viene considerato in modo errato per TDD (Test Driven Development).


4
Grazie. Lo vedo. Mi sto solo chiedendo, come posso quindi eseguire test di integrazione usando la simulazione in cui potrebbero esserci molti metodi interni, classi che potrebbero aver bisogno di essere derise, ma non necessariamente disponibili per essere impostate prima tramite un setXXX ().
Anand Hemmige,

2
Utilizzare un framework di iniezione delle dipendenze, con una configurazione di prova. Elaborare un diagramma di sequenza del test di integrazione che si sta tentando di eseguire. Fattorizza il diagramma di sequenza negli oggetti che puoi effettivamente controllare. Ciò significa che se stai lavorando con una classe di framework che ha l'anti-pattern dell'oggetto dipendente che mostri sopra, allora dovresti considerare l'oggetto e il suo membro mal fatto come una singola unità in termini di diagramma di sequenza. Preparati a modificare il factoring di qualsiasi codice che controlli, per renderlo più testabile.
Kittylyst

9
Caro @kittylyst, sì, probabilmente è sbagliato dal punto di vista TDD o da qualsiasi tipo di punto di vista razionale. Ma a volte uno sviluppatore lavora in luoghi in cui nulla ha assolutamente senso e l'unico obiettivo che uno ha è solo quello di completare le storie che hai assegnato e andare via. Sì, è sbagliato, non ha senso, le persone non qualificate prendono le decisioni chiave e tutto il resto. Quindi, alla fine della giornata, gli anti-pattern vincono molto.
amanas,

1
Sono curioso di questo, se un membro della classe non ha motivo di essere impostato dall'esterno, perché dovremmo creare un setter solo allo scopo di testarlo? Immagina che la 'Seconda' classe qui sia in realtà un gestore o uno strumento di FileSystem, inizializzato durante la costruzione dell'oggetto da testare. Ho tutti i motivi per voler deridere questo gestore del FileSystem, al fine di testare la First class e zero motivi per renderlo accessibile. Posso farlo in Python, quindi perché non con Mockito?
Zangdar,

65

Ciò non è possibile se non è possibile modificare il codice. Ma mi piace l'iniezione di dipendenza e Mockito la supporta:

public class First {    
    @Resource
    Second second;

    public First() {
        second = new Second();
    }

    public String doSecond() {
        return second.doSecond();
    }
}

Il tuo test:

@RunWith(MockitoJUnitRunner.class)
public class YourTest {
   @Mock
   Second second;

   @InjectMocks
   First first = new First();

   public void testFirst(){
      when(second.doSecond()).thenReturn("Stubbed Second");
      assertEquals("Stubbed Second", first.doSecond());
   }
}

Questo è molto carino e facile.


2
Penso che questa sia una risposta migliore rispetto alle altre perché InjectMocks.
sudocoder

È divertente come uno, in quanto principiante in prova come me, si fidi di certe librerie e framework. Supponevo che questa fosse solo una cattiva idea che indicava la necessità di riprogettare ... fino a quando non mi hai mostrato che è davvero possibile (molto chiaramente e in modo pulito) in Mockito.
mike rodent,

9
Cos'è @Resource ?
IgorGanapolsky,

3
@IgorGanapolsky @ Resource è un'annotazione creata / utilizzata dal framework Java Spring. È un modo per indicare a Spring che è un bean / oggetto gestito da Spring. stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire Non è una cosa Mockito, ma perché è usato nella classe non collaudo deve essere preso in giro in test.
Grez.Kev

Non capisco questa risposta. Dici che non è possibile, allora dimostri che è possibile? Cosa esattamente non è possibile qui?
Goldname

35

Se osservi attentamente il tuo codice, vedrai che la secondproprietà nel tuo test è ancora un'istanza Second, non una simulazione (non passi la derisione firstnel tuo codice).

Il modo più semplice sarebbe quello di creare un setter per secondin Firstclasse e passarlo in modo esplicito.

Come questo:

public class First {

Second second ;

public First(){
    second = new Second();
}

public String doSecond(){
    return second.doSecond();
}

    public void setSecond(Second second) {
    this.second = second;
    }


}

class Second {

public String doSecond(){
    return "Do Something";
}
}

....

public void testFirst(){
Second sec = mock(Second.class);
when(sec.doSecond()).thenReturn("Stubbed Second");


First first = new First();
first.setSecond(sec)
assertEquals("Stubbed Second", first.doSecond());
}

Un altro sarebbe passare Secondun'istanza come Firstparametro del costruttore.

Se non riesci a modificare il codice, penso che l'unica opzione sarebbe quella di usare reflection:

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");


    First first = new First();
    Field privateField = PrivateObject.class.
        getDeclaredField("second");

    privateField.setAccessible(true);

    privateField.set(first, sec);

    assertEquals("Stubbed Second", first.doSecond());
}

Ma probabilmente puoi, dato che è raro fare test su codice che non controlli (anche se uno può immaginare uno scenario in cui devi testare una libreria esterna perché l'autore non l'ha fatto :))


Fatto. Probabilmente seguirò il tuo primo suggerimento.
Anand Hemmige,

Solo curioso, c'è qualche modo o API che conosci che può deridere un oggetto / metodo a livello di applicazione o di pacchetto. ? Immagino che ciò che sto dicendo sia, nell'esempio sopra, quando derido un oggetto "Second", esiste un modo per sovrascrivere ogni istanza di Second che viene utilizzata durante il ciclo di vita dei test. ?
Anand Hemmige,

@AnandHemmige in realtà il secondo (costruttore) è più pulito, in quanto evita di creare istanze "second" non necessarie. Le tue classi vengono ben disaccoppiate in quel modo.
soulcheck

10
Mockito fornisce alcune belle annotazioni per permetterti di iniettare le tue beffe in variabili private. Annota Second con @Mocke annota First con @InjectMockse crea un'istanza First nell'inizializzatore. Mockito farà automaticamente del suo meglio per trovare un posto per iniettare il Secondo mock nella Prima istanza, inclusa l'impostazione di campi privati ​​che corrispondono al tipo.
jhericks,

@Mockera in giro in 1.5 (forse prima, non ne sono sicuro). 1.8.3 introdotto @InjectMocks, nonché @Spye @Captor.
jhericks,

7

Se non riesci a modificare la variabile membro, viceversa è utilizzare PowerMockit e chiamare

Second second = mock(Second.class)
when(second.doSecond()).thenReturn("Stubbed Second");
whenNew(Second.class).withAnyArguments.thenReturn(second);

Ora il problema è che QUALSIASI chiamata al nuovo Second restituirà la stessa istanza derisa. Ma nel tuo caso semplice funzionerà.


6

Ho avuto lo stesso problema in cui non è stato impostato un valore privato perché Mockito non chiama i super costruttori. Ecco come aumento beffardo con la riflessione.

Innanzitutto, ho creato una classe TestUtils che contiene molti utili programmi di utilità inclusi questi metodi di riflessione. L'accesso alla riflessione è un po 'complicato da implementare ogni volta. Ho creato questi metodi per testare il codice su progetti che, per un motivo o per l'altro, non avevano un pacchetto di simulazione e non sono stato invitato a includerlo.

public class TestUtils {
    // get a static class value
    public static Object reflectValue(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(classToReflect, fieldNameValueToFetch);
            reflectField.setAccessible(true);
            Object reflectValue = reflectField.get(classToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // get an instance value
    public static Object reflectValue(Object objToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameValueToFetch);
            Object reflectValue = reflectField.get(objToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // find a field in the class tree
    public static Field reflectField(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField = null;
            Class<?> classForReflect = classToReflect;
            do {
                try {
                    reflectField = classForReflect.getDeclaredField(fieldNameValueToFetch);
                } catch (NoSuchFieldException e) {
                    classForReflect = classForReflect.getSuperclass();
                }
            } while (reflectField==null || classForReflect==null);
            reflectField.setAccessible(true);
            return reflectField;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch +" from "+ classToReflect);
        }
        return null;
    }
    // set a value with no setter
    public static void refectSetValue(Object objToReflect, String fieldNameToSet, Object valueToSet) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameToSet);
            reflectField.set(objToReflect, valueToSet);
        } catch (Exception e) {
            fail("Failed to reflectively set "+ fieldNameToSet +"="+ valueToSet);
        }
    }

}

Quindi posso testare la classe con una variabile privata come questa. Ciò è utile per deridere alberi profondi che non hai alcun controllo.

@Test
public void testWithRectiveMock() throws Exception {
    // mock the base class using Mockito
    ClassToMock mock = Mockito.mock(ClassToMock.class);
    TestUtils.refectSetValue(mock, "privateVariable", "newValue");
    // and this does not prevent normal mocking
    Mockito.when(mock.somthingElse()).thenReturn("anotherThing");
    // ... then do your asserts
}

Ho modificato il mio codice dal mio progetto attuale qui, nella pagina. Potrebbe esserci un problema di compilazione o due. Penso che tu abbia l'idea generale. Sentiti libero di prendere il codice e usarlo se lo trovi utile.


Potresti spiegare il tuo codice con un caso d'uso reale? come Public class tobeMocker () {private ClassObject classObject; } Dove classObject è uguale all'oggetto da sostituire.
Jasper Lankhorst,

Nel tuo esempio, se l'istanza di ToBeMocker = new ToBeMocker (); e ClassObject someNewInstance = new ClassObject () {@Override // qualcosa come una dipendenza esterna}; quindi TestUtils.refelctSetValue (istanza, "classObject", someNewInstance); Nota che devi capire cosa vuoi scavalcare per deridere. Supponiamo che tu abbia un database e questa sostituzione restituirà un valore, quindi non è necessario selezionarlo. Più recentemente ho avuto un bus di servizio che non volevo elaborare effettivamente il messaggio, ma volevo assicurarmi che lo ricevesse. Pertanto, ho impostato l'istanza del bus privato in questo modo: utile?
Dave,

Dovrai immaginare che c'era formattazione in quel commento. È stato rimosso. Inoltre, questo non funzionerà con Java 9 in quanto bloccherà l'accesso privato. Dovremo lavorare con alcuni altri costrutti una volta che abbiamo una versione ufficiale e possiamo lavorare con i suoi limiti attuali.
Dave,

1

Molti altri ti hanno già consigliato di ripensare il tuo codice per renderlo più testabile: un buon consiglio e di solito più semplice di quello che sto per suggerire.

Se non riesci a modificare il codice per renderlo più testabile, PowerMock: https://code.google.com/p/powermock/

PowerMock estende Mockito (quindi non devi imparare un nuovo framework simulato), fornendo funzionalità aggiuntive. Ciò include la possibilità che un costruttore abbia restituito una simulazione. Potente, ma un po 'complicato, quindi usalo con giudizio.

Si utilizza un corridore Mock diverso. E devi preparare la classe che invocherà il costruttore. (Nota che questo è un gotcha comune: prepara la classe che chiama il costruttore, non la classe costruita)

@RunWith(PowerMockRunner.class)
@PrepareForTest({First.class})

Quindi, nella configurazione del test, è possibile utilizzare il metodo whenNew per fare in modo che il costruttore restituisca una simulazione

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class));

0

Sì, questo può essere fatto, come mostra il seguente test (scritto con l'API di derisione JMockit, che sviluppo):

@Test
public void testFirst(@Mocked final Second sec) {
    new NonStrictExpectations() {{ sec.doSecond(); result = "Stubbed Second"; }};

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Con Mockito, tuttavia, tale test non può essere scritto. Ciò è dovuto al modo in cui il mocking viene implementato in Mockito, dove viene creata una sottoclasse della classe da deridere; solo le istanze di questa sottoclasse "finta" possono avere comportamenti derisi, quindi è necessario che il codice testato le usi invece di qualsiasi altra istanza.


3
la domanda non era se JMockit fosse migliore di Mockito, ma piuttosto come farlo in Mockito. Attenersi alla costruzione di un prodotto migliore invece di cercare opportunità per rovinare la concorrenza!
TheZuck

8
Il poster originale dice solo che sta usando Mockito; è solo implicito che Mockito è un requisito fisso e difficile, quindi il suggerimento che JMockit può gestire questa situazione non è così inappropriato.
Bombe

0

Se vuoi un'alternativa a ReflectionTestUtils di Spring in mockito, usa

Whitebox.setInternalState(first, "second", sec);

Benvenuto in Stack Overflow! Esistono altre risposte che forniscono la domanda del PO e sono state pubblicate molti anni fa. Quando pubblichi una risposta, assicurati di aggiungere una nuova soluzione o una spiegazione sostanzialmente migliore, in particolare quando rispondi a domande precedenti o fai commenti su altre risposte.
help-info.de
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.