2012-01-24 4 views
91

Sono un principiante dello sviluppo e in particolare dei test unitari. Suppongo che il mio requisito sia piuttosto semplice, ma sono desideroso di conoscere altri pensieri su questo.Mocking delle variabili membro di una classe usando Mockito

Supponiamo che io ho due classi in questo modo -

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 io sto scrivendo unit test per testare First.doSecond() metodo. Tuttavia, supponiamo, voglio prendere in giro la classe Second.doSecond() in questo modo. Sto usando Mockito per farlo.

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

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

sto vedendo che il beffardo non ha effetto e l'asserzione fallisce. Non c'è modo di prendere in giro le variabili membro di una classe che voglio testare. ?

risposta

58

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

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

+1

Grazie. Lo vedo. Mi sto solo chiedendo come posso eseguire test di integrazione usando mock dove ci possono essere molti metodi interni, classi che potrebbero aver bisogno di essere deriso, ma non necessariamente disponibili per essere impostate attraverso un setXXX() in anticipo. –

+2

Utilizzare un framework di iniezione delle dipendenze, con una configurazione di test. Disegna un diagramma di sequenza del test di integrazione che stai cercando di fare. Calcola il diagramma di sequenza negli oggetti che puoi effettivamente controllare. Ciò significa che se stai lavorando con una classe framework che ha l'anti-pattern dell'oggetto dipendente che mostri sopra, allora dovresti considerare l'oggetto e il suo membro malamente fattorizzato come una singola unità in termini del diagramma di sequenza. Preparati a modificare il factoring di qualsiasi codice che controlli, per renderlo più testabile. – kittylyst

+4

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 posti dove nulla ha senso e l'unico obiettivo che si 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 tutte queste cose. Quindi, alla fine della giornata, gli anti-pattern vincono molto. – amanas

30

Se si guarda attentamente il codice vedrete che la proprietà second nel vostro test è ancora un esempio di Second, non un finto (non si passa il mock per first nel codice).

Il modo più semplice sarebbe quello di creare un setter per second nella classe First e passarlo esplicitamente al mock.

Ti piace questa:

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'altra potrebbe essere quella passare un'istanza Second come parametro del costruttore First s'.

Se non è possibile modificare il codice, credo che l'unica opzione sarebbe quella di utilizzare la riflessione:

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 si può, come è raro fare test sul codice che non controlli (sebbene si possa immaginare uno scenario in cui si deve testare una libreria esterna perché l'autore non lo ha fatto :))

+0

Capito. Probabilmente andrò con il tuo primo suggerimento. –

+0

Solo curioso, c'è qualche modo o API di cui sei a conoscenza che può prendere in giro un oggetto/metodo a livello di applicazione o di pacchetto. ? Immagino che quello che sto dicendo sia, nell'esempio sopra quando faccio finta di "Second" object, c'è un modo in cui può sovrascrivere ogni istanza di Second che viene utilizzata attraverso il ciclo di vita dei test. ? –

+0

@AnandHemmige in realtà il secondo (costruttore) è più pulito, in quanto evita di creare istanze secondarie 'second'. Le tue lezioni si disaccoppiano in questo modo. – soulcheck

42

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

public class First {  
    @Resource 
    Second second; 

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

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

Il 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 bello e facile.

+0

Penso che questa sia una risposta migliore rispetto a quella degli altri InjectMocks. – sudocoder

+0

È strano come si possa, come un principiante di test come me, fidarsi di certe librerie e framework. Supponevo che questa fosse solo una cattiva idea che indicava la necessità di riprogettare ... finché non me lo hai mostrato ** è effettivamente possibile (molto chiaramente e in modo pulito) in Mockito. –

+3

Che cos'è ** @ Risorsa **? –

-1

Sì, questo può essere fatto, come mostrato nella seguente prova (scritta con il JMockit beffardo API, quali progetto):

@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 è implementato in Mockito, dove viene creata una sottoclasse della classe da deridere; solo le istanze di questa sottoclasse "simulata" possono avere un comportamento fittizio, quindi è necessario che il codice testato li usi al posto di qualsiasi altra istanza.

+3

la domanda non era se JMockit fosse meglio di Mockito, ma piuttosto come farlo in Mockito. Attenersi alla costruzione di un prodotto migliore invece di cercare l'opportunità di distruggere la concorrenza! – TheZuck

+8

Il poster originale dice solo che sta usando Mockito; è solo sottinteso che Mockito è un requisito fisso e rigido, quindi il suggerimento che JMockit possa gestire questa situazione non è inappropriato. – Bombe

6

Se non è possibile modificare la variabile membro, poi l'altro modo per aggirare questo è quello di 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 alla nuova Seconda restituirà la stessa istanza deriso. Ma nel tuo caso semplice funzionerà.

1

Molti altri hanno già consigliato di riconsiderare il codice per renderlo più verificabile - un buon consiglio e di solito più semplice di quello che sto per suggerire.

Se non è possibile modificare il codice per renderlo più verificabili, PowerMock: https://code.google.com/p/powermock/

PowerMock estende Mockito (in modo da non dover imparare un nuovo quadro finta), che fornisce funzionalità aggiuntive. Ciò include la possibilità di avere un costruttore che restituisce un mock. Potente, ma un po 'complicato, quindi usalo con giudizio.

Si utilizza un diverso finto runner. E devi preparare la classe che invocherà il costruttore. (Si noti che questo è un Gotcha comune - preparare la classe che chiama il costruttore, non la classe costruito)

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

Poi, nel tuo test di set-up, è possibile utilizzare il metodo whenNew di avere il costruttore restituire un mock

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

ho avuto lo stesso problema in cui un valore privato non è stata impostata perché Mockito non chiama i costruttori di super. Ecco come mi diletto a riflettere.

In primo luogo, ho creato una classe TestUtils che contiene molti util utili tra cui questi metodi di riflessione. L'accesso alla riflessione è un po 'improbabile da implementare ogni volta. Ho creato questi metodi per testare il codice su progetti che, per una ragione o per l'altra, non avevano alcun pacchetto di derisione 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. Questo è utile per deridere gli alberi profondi che non hai 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. Ci potrebbe essere 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.

+0

Puoi spiegare il tuo codice con un vero e proprio prodotto? come classe pubblica tobeMocker() { ClassObject classObject privato; } Dove classObject è uguale all'oggetto da sostituire. –

+0

Nell'esempio, se istanza ToBeMocker = new ToBeMocker(); e ClassObject someNewInstance = new ClassObject() { @Override // qualcosa come una dipendenza esterna }; quindi TestUtils.refelctSetValue (istanza, "classObject", someNewInstance); Nota che devi capire che cosa vuoi sovrascrivere per deridere. Diciamo che hai un database e questo sostituirà un valore in modo da non dover selezionare. Più di recente ho avuto un bus di servizio che non volevo effettivamente elaborare il messaggio ma volevo assicurarmi che lo ricevesse. Pertanto, ho impostato l'istanza del bus privato in questo modo: utile? – dave

+0

Dovresti immaginare che ci fosse una formattazione in quel commento. È stato rimosso Inoltre, questo non funzionerà con Java 9 poiché bloccherà l'accesso privato. Dovremo lavorare con alcuni altri costrutti una volta che avremo una versione ufficiale e possiamo lavorare con i suoi limiti effettivi. – dave