2011-12-22 20 views
10

Quindi mi è stato chiesto di leggere su beffe e BDD per il nostro team di sviluppo e di giocare con i mock in modo da migliorare una manciata dei nostri test unitari esistenti (come esperimento).Mockito: derisione "Blackbox" Dipendenze

Alla fine ho scelto di andare con Mockito per una serie di motivi (alcuni al di fuori della portata del mio controllo), ma in particolare perché supporta sia lo stubing che il mocking per le istanze in cui il mocking non sarebbe appropriato.

Ho passato tutto il giorno a studiare su Mockito, beffardo (in generale) e BDD. E ora sono pronto per scavare e iniziare ad aumentare i nostri test unitari.

Così abbiamo una classe denominata WebAdaptor che ha un metodo run():

public class WebAdaptor { 

    private Subscriber subscriber; 

    public void run() { 

     subscriber = new Subscriber(); 
     subscriber.init(); 
    } 
} 

Si prega di notare: (! Per motivi al di fuori della portata di questa domanda) Non ho un modo per modificare questo codice . Così faccio non ho la possibilità di aggiungere un metodo setter per Subscriber, e quindi può essere pensato come un "blackbox" irraggiungibile all'interno del mio WebAdaptor.

Voglio scrivere uno unit test che incorpora un finto Mockito, e lo utilizza per finta verify che l'esecuzione WebAdaptor::run() provoca Subscriber::init() di essere chiamato.

Quindi, ecco quello che ho finora (all'interno WebAdaptorUnitTest):

@Test 
public void runShouldInvokeSubscriberInit() { 

    // Given 
    Subscriber mockSubscriber = mock(Subscriber.class); 
    WebAdaptor adaptor = new WebAdaptor(); 

    // When 
    adaptor.run(); 

    // Then 
    verify(mockSubscriber).init(); 
} 

Quando ho eseguito questo test, il metodo effettivo Subscriber::init() viene eseguito (vi posso dire dall'uscita della console e dei file vedendo generato sul mio sistema locale), non il mockSubscriber, che non dovrebbe fare (o restituire) nulla.

Ho controllato e ricontrollato: init è public, non è né static o final, e restituisce void. Secondo i documenti, Mockito non dovrebbe avere problemi a deridere questo oggetto.

Quindi mi ha fatto pensare: devo associare esplicitamente lo mockSubscriber allo adaptor? Se questo è un caso, allora di solito, quanto segue normalmente risolvere il problema:

adaptor.setSubscriber(mockSubscriber); 

Ma dal momento che non posso aggiungere tale setter (si prega di leggere la mia nota di cui sopra), sono ad una perdita quanto a come avrei potuto forza una tale associazione. Quindi, diverse domande strettamente correlate:

  • Qualcuno può confermare che ho impostato correttamente il test (utilizzando l'API Mockito)?
  • Il mio sospetto riguardo al setter mancante è corretto? (Devo associare questi oggetti tramite un setter?)
  • Se il mio sospetto sopra è vero, e non posso modificare WebAdaptor, ci sono delle circostanze a mia disposizione?

Grazie in anticipo!

+0

Questo non risponde direttamente alla tua domanda, ma JMockit rende questo tipo di black-box beffardo abbastanza facile. JMockIt è un'opzione per te? –

+0

In che modo il Sottoscrittore viene istanziato in questa classe? È possibile sovrascrivere il codice di istanziazione per restituire un'istanza che controlli? –

+0

run() è l'unico metodo che utilizza il Sottoscrittore, quindi con tutti i mezzi dovrebbe essere una variabile locale all'interno di quel metodo. Ancora una volta, non posso modificare il codice ... – IAmYourFaja

risposta

10

È necessario inserire la simulazione nella classe che si sta testando.Non è necessario l'accesso al Sottoscrittore. Il modo in cui mockito e altri framework di derisione aiutano è che non è necessario accedere agli oggetti con i quali si sta interagendo. Tuttavia, è necessario un modo per ottenere oggetti simulati nella classe che si sta testando.

public class WebAdaptor { 

    public WebAdaptor(Subscriber subscriber) { /* Added a new constructor */ 
     this.subscriber = subscriber; 
    } 

    private Subscriber subscriber; 

    public void run() { 
     subscriber.init(); 
    } 
} 

Ora è possibile verificare le interazioni sul modello piuttosto che sull'oggetto reale.

@Test 
public void runShouldInvokeSubscriberInit() { 

    // Given 
    Subscriber mockSubscriber = mock(Subscriber.class); 
    WebAdaptor adaptor = new WebAdaptor(mockSubscriber); // Use the new constructor 

    // When 
    adaptor.run(); 

    // Then 
    verify(mockSubscriber).init(); 
} 

Se aggiungendo l'Abbonato al costruttore non è l'approccio corretto, si potrebbe anche considerare l'utilizzo di una fabbrica per consentire WebAdaptor istanziare nuovi oggetti Subscriber da una fabbrica cui è possibile controllare. Potresti quindi prendere in giro la fabbrica con il simulatore di abbonati.

+0

Il frammento per WebAdaptor ha ancora il metodo run() che crea un nuovo Sottoscrittore. FYI. –

+0

David V - dopo aver ascoltato ad alta voce (come ho scritto questa domanda), combinato con la tua risposta e i commenti sopra, sembra che cambiare il codice di 'WebAdaptor' è la mia unica opzione. Grazie per la risposta! – IAmYourFaja

+2

Per mantenere la classe WebAdaptor funzionante con il codice esistente, potresti anche volere un costruttore no-arg che chiami il tuo nuovo costruttore. Quindi gli attuali usi non test della classe possono utilizzare il costruttore no-arg. Quindi il nuovo costruttore sarebbe 'public WebAdaptor() {this (new Subscriber());}'. Inoltre, il costruttore con l'argomento Sottoscrittore dovrebbe essere pacchetto-privato. –

5

Se non si desidera modificare il codice di produzione ed essere ancora in grado di simulare la funzionalità della classe Subscriber, è necessario dare un'occhiata a PowerMock. Funziona bene insieme a Mockito e ti permette di prendere in giro la creazione di nuovi oggetti.

Subscriber mockSubscriber = mock(Subscriber.class); 
whenNew(Subscriber.class).withNoArguments().thenReturn(mockSubscriber); 

Ulteriori dettagli sono illustrati nel framework documentation for the PowerMock.

+0

Leggi [Esempio # 3 - Costruzione fittizia di nuovi oggetti] (http://blog.jayway.com/2009/10/28/untestable-code-with-mockito-and-powermock/) (scorrere verso il basso) per un altro esempio esaustivo. – matsev

2

C'è un modo per iniettare il tuo mock nella classe sotto test senza apportare modifiche al codice. Questo può essere fatto usando il Mockito WhiteBox. Questa è un'ottima funzionalità che può essere utilizzata per iniettare le dipendenze della tua Classe sotto test dai tuoi test. Di seguito è riportato un semplice esempio su come funziona,

@Mock 
Subscriber mockSubscriber; 
WebAdaptor cut = new WebAdaptor(); 

@Before 
public void setup(){ 
    //sets the internal state of the field in the class under test even if it is private 
    MockitoAnnotations.initMocks(this); 

    //Now the whitebox functionality injects the dependent object - mockSubscriber 
    //into the object which depends on it - cut 
    Whitebox.setInternalState(cut, "subscriber", mockSubscriber); 
} 

@Test 
public void runShouldInvokeSubscriberInit() { 
    cut.run(); 
    verify(mockSubscriber).init(); 
} 

Spero che questo aiuti :-)

+0

@zharvey è stato utile? o stavo sbagliando? – Bala

+2

Questo non funzionerà. Quando cut.run() viene chiamata una nuova istanza Subscriber non simulata, verrà creata all'interno di WebAdapter e sostituirà la versione derisoria. Verrà quindi chiamato il metodo init sulla nuova istanza. –

+0

Sì, hai ragione. L'ho perso – Bala

1

Non si può prendere in giro l'Abbonato utilizzando Mockito nell'implementazione corrente.

Il problema è che il Sottoscrittore è costruito e quindi immediatamente accessibile, Mockito non ha la capacità di sostituire (o spiare) l'istanza Sottoscrittore dopo la creazione ma prima che venga chiamato il metodo init.

public void run() { 

    subscriber = new Subscriber(); 
    // Mockito would need to jump in here 
    subscriber.init(); 
} 

La risposta di David V risolve questo aggiungendo il Sottoscrittore al costruttore. Un'alternativa che mantiene la struttura nascosta del Sottoscrittore sarebbe quella di creare un'istanza del Sottoscrittore in un costruttore no-arg di WebAdapter e quindi utilizzare il reflection per sostituire quell'istanza prima di chiamare il metodo run.

tuo WebAdapter sarebbe simile a questa,

public class WebAdaptor { 

    private Subscriber subscriber; 

    public WebAdaptor() { 
     subscriber = new Subscriber(); 
    } 

    public void run() {    
     subscriber.init(); 
    } 
} 

E si potrebbe usare ReflectionTestUtils dal modulo di test di Spring Framework per iniettare le dipendenze in quel campo privato.

@Test 
public void runShouldInvokeSubscriberInit() { 

    // Given 
    Subscriber mockSubscriber = mock(Subscriber.class); 
    WebAdaptor adaptor = new WebAdaptor(); 
    ReflectionTestUtils.setField(adaptor "subscriber", mockSubscriber); 

    // When 
    adaptor.run(); // This will call mockSubscriber.init() 

    // Then 
    verify(mockSubscriber).init(); 
} 

ReflectionTestUtils è in realtà solo un wrapper su riflessione di Java, lo stesso potrebbe essere realizzato manualmente (e molto altro verbosely) senza la dipendenza di primavera.

Mockito's WhiteBox (come suggerisce Bala) funzionerebbe qui al posto di ReflectionTestUtils, è contenuto all'interno del pacchetto interno di Mockito, quindi mi allontano da esso, YMMV.

2

si potrebbe avere usato PowerMock per deridere la chiamata al costruttore senza modificare il codice originale:

import org.mockito.Mockito; 
import org.powermock.api.mockito.PowerMockito; 
import org.powermock.core.classloader.annotations.PrepareForTest; 
import org.powermock.modules.junit4.PowerMockRunner; 

@RunWith(PowerMockRunner.class) 
@PrepareForTest(WebAdaptor.class) 
public class WebAdaptorTest { 
    @Test 
    public void testRunCallsSubscriberInit() { 
     final Subscriber subscriber = mock(Subscriber.class); 
     whenNew(Subscriber.class).withNoArguments().thenReturn(subscriber); 
     new WebAdaptor().run(); 
     verify(subscriber).init(); 
    } 
}