17

Ho recentemente passato intere con Dagger perché il concetto di DI ha perfettamente senso. Uno dei "sottoprodotti" più belli di DI (come ha fatto Jake Wharton in una delle sue presentazioni) è la facilità di testabilità.ottenere Dagger per iniettare oggetti finti quando si eseguono test funzionali dell'espresso per Android

Così ora sto praticamente usando espresso per fare alcuni test funzionali, e voglio essere in grado di iniettare dati fittizi/mock all'applicazione e fare in modo che l'attività li mostri. Sto indovinando poiché, questo è uno dei maggiori vantaggi di DI, questa dovrebbe essere una domanda relativamente semplice. Per qualche motivo, però, non riesco a spiegarmelo. Qualsiasi aiuto sarebbe molto apprezzato. Ecco quello che ho finora (ho scritto su un esempio che riflette il mio attuale configurazione):

public class MyActivity 
    extends MyBaseActivity { 

    @Inject Navigator _navigator; 

    @Override 
    public void onCreate(Bundle savedInstanceState) { 
     super.onCreate(savedInstanceState); 
     MyApplication.get(this).inject(this); 

     // ... 

     setupViews(); 
    } 

    private void setupViews() { 
     myTextView.setText(getMyLabel()); 
    } 

    public String getMyLabel() { 
     return _navigator.getSpecialText(); // "Special Text" 
    } 
} 

Questi sono i miei moduli pugnale:

// Navigation Module 

@Module(library = true) 
public class NavigationModule { 

    private Navigator _nav; 

    @Provides 
    @Singleton 
    Navigator provideANavigator() { 
     if (_nav == null) { 
      _nav = new Navigator(); 
     } 
     return _nav; 
    } 
} 

// App level module 

@Module(
    includes = { SessionModule.class, NavigationModule.class }, 
    injects = { MyApplication.class, 
       MyActivity.class, 
       // ... 
}) 
public class App { 
    private final Context _appContext; 
    AppModule(Context appContext) { 
     _appContext = appContext; 
    } 
    // ... 
} 

Nella mia prova Espresso, sto cercando per inserire un modulo falso in questo modo:

public class MyActivityTest 
    extends ActivityInstrumentationTestCase2<MyActivity> { 

    public MyActivityTest() { 
     super(MyActivity.class); 
    } 

    @Override 
    public void setUp() throws Exception { 
     super.setUp(); 
     ObjectGraph og = ((MyApplication) getActivity().getApplication()).getObjectGraph().plus(new TestNavigationModule()); 
     og.inject(getActivity()); 
    } 

    public void test_SeeSpecialText() { 
     onView(withId(R.id.my_text_view)).check(matches(withText(
      "Special Dummy Text))); 
    } 

    @Module(includes = NavigationModule.class, 
      injects = { MyActivityTest.class, MyActivity.class }, 
      overrides = true, 
      library = true) 
    static class TestNavigationModule { 

     @Provides 
     @Singleton 
     Navigator provideANavigator() { 
      return new DummyNavigator(); // that returns "Special Dummy Text" 
     } 
    } 
} 

Questo non funziona affatto. I miei test per il caffè funzionano, ma il TestNavigationModule è completamente ignorato ... arr ... :(

Che cosa sto sbagliando? Esiste un approccio migliore per deridere i moduli con Espresso. Ho cercato e visto esempi di Robolectric, Mockito ecc utilizzato ma voglio solo test Espresso puri e la necessità di scambiare un modulo con la mia una finta Come dovrei fare questo

EDIT:..?

così sono andato con @ user3399328 approccio di avere una definizione di elenco di moduli di test statici, verificare la presenza di null e quindi aggiungerli nella mia classe Application. Non sto ancora ottenendo la versione di Injected Test della classe, ma ho la sensazione che probabilmente è qualcosa di sbagliato con la definizione del modulo test del pugnale e non il mio ciclo di vita del caffè espresso. La ragione per cui sto facendo l'ipotesi è che aggiungo le istruzioni di debug e trovo che il modulo di test statico non è vuoto al momento dell'iniezione nella classe dell'applicazione. Potresti indicarmi la direzione di ciò che potrei eventualmente fare male. Qui ci sono frammenti di codice delle mie definizioni:

MyApplication:

@Override 
public void onCreate() { 
    // ... 
    mObjectGraph = ObjectGraph.create(Modules.list(this)); 
    // ... 
} 

moduli:

public class Modules { 

    public static List<Object> _testModules = null; 

    public static Object[] list(MyApplication app) { 
     //  return new Object[]{ new AppModule(app) }; 
     List<Object> modules = new ArrayList<Object>(); 
     modules.add(new AppModule(app)); 

     if (_testModules == null) { 
      Log.d("No test modules"); 
     } else { 
      Log.d("Test modules found"); 
     } 

     if (_testModules != null) { 
      modules.addAll(_testModules); 
     } 

     return modules.toArray(); 
    } 
} 

modulo di test modificato nel mio classe di test:

@Module(overrides = true, library = true) 
public static class TestNavigationModule { 

    @Provides 
    @Singleton 
    Navigator provideANavigator()() { 
     Navigator navigator = new Navigator(); 
     navigator.setSpecialText("Dummy Text"); 
     return navigator; 
    } 
} 

risposta

8

L'approccio non funziona perché accade solo una volta e, come menzionato da Matt, quando viene eseguito il codice di iniezione reale dell'attività, cancellerà tutte le variabili immesse dal grafico dell'oggetto speciale.

Ci sono due modi per farlo funzionare.

Il modo più rapido: fare una variabile statica pubblica nella vostra attività in modo un test può assegnare un modulo di sostituzione e hanno il codice di attività effettiva includono sempre questo modulo se non è nulla (che avverrà solo in test). È simile alla mia risposta here solo per la classe base di attività anziché per l'applicazione.

Il modo più lungo, probabilmente migliore: refactoring del codice in modo che tutte le attività di iniezione (e, soprattutto, creazione del grafico) avvengano in una classe, ad esempio ActivityInjectHelper. Nel pacchetto di test, creare un'altra classe denominata ActivityInjectHelper con lo stesso identico metodo percorso di pacchetto che implementa gli stessi metodi, ad eccezione dei moduli di test. Poiché le classi di test vengono caricate per prime, l'applicazione verrà eseguita con il test ActivityInjectHelper. Ancora una volta è simile alla mia risposta here solo per una classe diversa.

UPDATE:

Vedo che hai postato più di codice ed è vicino a lavorare, ma non sigaro. Per entrambe le attività e le applicazioni, è necessario inserire il modulo di test prima dell'esecuzione di onCreate(). Quando si tratta di grafici di oggetti di attività, in qualsiasi momento prima che getActivity() del test sia soddisfacente. Quando si ha a che fare con le applicazioni, è un po 'più difficile perché onCreate() è già stato chiamato dal momento dell'esecuzione di setUp(). Fortunatamente, farlo nel costruttore del test funziona - l'applicazione non è stata creata a quel punto. Ne cito brevemente nel mio primo link.

+1

Questa è la strada da percorrere. Un modo ingegnoso per aggirare la limitazione. Oscilla signore! –

+0

Quando provo a iniettare i miei moduli di test nel costruttore della mia sottoclasse ActivityInstrumentationTestCase2, non funziona perché l'applicazione è già stata istanziata e onCreate è già stato chiamato. Quindi non ho ancora trovato un buon metodo per dichiarare i miei moduli di test durante l'esecuzione dei test –

+0

Attualmente sto provando la seconda soluzione di @ user3399328 (fornendo un'altra versione di ActivityInjectHelper nelle mie origini di test). Hai trovato un modo per farlo funzionare con Gradle? Continuo a correre nella classe 'Duplicate trovata negli errori del file [nome file]' che compaiono in Android Studio. – Ben

1

La chiamata a getActivity volontà in realtà inizia la tua attività chiamando su Crea nel processo, il che significa che non avrai i tuoi tes t moduli aggiunti al grafico in tempo utile. Utilizzando activityInstrumentationTestcase2 non è possibile iniettare realmente correttamente nell'ambito dell'attività. Ho lavorato su questo utilizzando la mia applicazione per fornire dipendenze alle mie attività e quindi iniettare oggetti mock in esso che le attività useranno. Non è l'ideale, ma funziona. È possibile utilizzare un bus eventi come Otto per contribuire a fornire dipendenze.

0

EDIT: il seguito in forma dopo http://systemdotrun.blogspot.co.uk/2014/11/android-testing-with-dagger-retrofit.html

per testare un Activity utilizzando Espresso + Pugnale ho fatto il seguito

Ispirato alla risposta da @ user3399328 Ho una classe DaggerHelper dentro la mia classe di applicazioni, che consente al caso di test di ignorare i @Provider s utilizzando il test @Modules che fornisce i mock. Finché

1) Questo viene fatto prima delle testcases getActivity() chiamata viene effettuata (come mia chiamata iniettare accade nella mia attività all'interno Activity.onCreate)

2) tearDown rimuove i moduli di prova dal grafo di oggetti.

Esempi di seguito.

Nota: questo non è l'ideale in quanto è soggetta alle insidie ​​simili utilizzando metodi di fabbrica per CIO, ma almeno in questo modo la sua sempre e solo una singola chiamata a tearDown() per portare il sistema in prova di nuovo al normale.

Il DaggerHelper dentro la mia classe Application

public static class DaggerHelper 
{ 
    private static ObjectGraph sObjectGraph; 

    private static final List<Object> productionModules; 

    static 
    { 
     productionModules = new ArrayList<Object>(); 
     productionModules.add(new DefaultModule()); 
    } 

    /** 
    * Init the dagger object graph with production modules 
    */ 
    public static void initProductionModules() 
    { 
     initWithModules(productionModules); 
    } 

    /** 
    * If passing in test modules make sure to override = true in the @Module annotation 
    */ 
    public static void initWithTestModules(Object... testModules) 
    { 
     initWithModules(getModulesAsList(testModules)); 
    } 

    private static void initWithModules(List<Object> modules) 
    { 
     sObjectGraph = ObjectGraph.create(modules.toArray()); 
    } 

    private static List<Object> getModulesAsList(Object... extraModules) 
    { 
     List<Object> allModules = new ArrayList<Object>(); 
     allModules.addAll(productionModules); 
     allModules.addAll(Arrays.asList(extraModules)); 
     return allModules; 
    } 

    /** 
    * Dagger convenience method - will inject the fields of the passed in object 
    */ 
    public static void inject(Object object) { 
     sObjectGraph.inject(object); 
    } 
} 

mio modulo di prova all'interno della mia classe di test

@Module (
     overrides = true, 
     injects = ActivityUnderTest.class 
) 
static class TestDataPersisterModule { 
    @Provides 
    @Singleton 
    DataPersister provideMockDataPersister() { 
     return new DataPersister(){ 
      @Override 
      public void persistDose() 
      { 
       throw new RuntimeException("Mock DI!"); //just a test to see if being called 
      } 
     }; 
    } 
} 

Metodo

public void testSomething() 
{ 
    MyApp.DaggerHelper.initWithTestModules(new TestDataPersisterModule()); 
    getActivity(); 
    ... 
} 

abbattere

012.
@Override 
public void tearDown() throws Exception 
{ 
    super.tearDown(); 
    //reset 
    MyApp.DaggerHelper.initProductionModules(); 
} 
8

Con Dagger 2 e Espresso 2 le cose sono effettivamente migliorate. Ecco come potrebbe ora essere un caso di test. Si noti che ContributorsModel è fornito da Dagger.La demo completa disponibile qui: https://github.com/pmellaaho/RxApp

@RunWith(AndroidJUnit4.class) 
public class MainActivityTest { 

ContributorsModel mModel; 

@Singleton 
@Component(modules = MockNetworkModule.class) 
public interface MockNetworkComponent extends RxApp.NetworkComponent { 
} 

@Rule 
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(
     MainActivity.class, 
     true,  // initialTouchMode 
     false); // launchActivity. 

@Before 
public void setUp() { 
    Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 
    RxApp app = (RxApp) instrumentation.getTargetContext() 
      .getApplicationContext(); 

    MockNetworkComponent testComponent = DaggerMainActivityTest_MockNetworkComponent.builder() 
      .mockNetworkModule(new MockNetworkModule()) 
      .build(); 
    app.setComponent(testComponent); 
    mModel = testComponent.contributorsModel(); 
} 

@Test 
public void listWithTwoContributors() { 

    // GIVEN 
    List<Contributor> tmpList = new ArrayList<>(); 
    tmpList.add(new Contributor("Jesse", 600)); 
    tmpList.add(new Contributor("Jake", 200)); 

    Observable<List<Contributor>> testObservable = Observable.just(tmpList); 

    Mockito.when(mModel.getContributors(anyString(), anyString())) 
      .thenReturn(testObservable); 

    // WHEN 
    mActivityRule.launchActivity(new Intent()); 
    onView(withId(R.id.startBtn)).perform(click()); 

    // THEN 
    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 0)) 
      .check(matches(hasDescendant(withText("Jesse")))); 

    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 0)) 
      .check(matches(hasDescendant(withText("600")))); 

    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 1)) 
      .check(matches(hasDescendant(withText("Jake")))); 

    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 1)) 
      .check(matches(hasDescendant(withText("200")))); 
} 
+2

Questo è anche il modo migliore che ho trovato. 1) esporre il contenitore DI sull'Applicazione 2) fare in modo che ActivityTestRule non avvii automaticamente l'app 3) modificare il contenitore DI nel metodo di test (o configurazione) 4) avviare l'app manualmente 5) test. – newfivefour

Problemi correlati