Testing di app Android con Dagger 2, Mockito e una rule JUnit

La Dependency Injection è la chiave per ottenere codice testabile, sfruttando questo costrutto è semplice sostituire un oggetto con un mock per cambiare e verificare il comportamento di una app Android o di un qualunque software.
Dagger 2 è una libreria che permette di gestire le proprie classi sfruttando la depenncy injection, per le sue caratteristiche (prima fra tutte la leggerezza) è usata in molte progetti Android. In Questo post vedremo come sfruttare Dagger 2 per testare una app Android.

Vediamo un esempio molto semplice, la classe MainService usa altre due classi per simulare una chiamata a un servizio esterno e stampare il risultato su console:

public class MainService {
    private RestService restService;
    private MyPrinter printer;

    @Inject public MainService(RestService restService, 
            MyPrinter printer) {
        this.restService = restService;
        this.printer = printer;
    }

    public void doSomething() {
        String s = restService.getSomething();
        printer.print(s.toUpperCase());
    }
}

Il metodo doSomething non ha input e output diretti ma, sfruttando la Dependency Injection e Mockito, non è difficile da testare.
L’implementazione delle altre classi è molto semplice:

public class RestService {
    public String getSomething() {
        return "Hello world";
    }
}
public class MyPrinter {
    public void print(String s) {
        System.out.println(s);
    }
}

Per poter testare la classe MainService in isolamento non stiamo usando l’annotation Inject su queste due classi (più avanti vedremo meglio questo dettaglio). All’interno di un modulo di Dagger creaiamo i due oggetti:

@Module
public class MyModule {
    @Provides @Singleton public RestService provideRestService() {
        return new RestService();
    }

    @Provides @Singleton public MyPrinter provideMyPrinter() {
        return new MyPrinter();
    }
}

E’ necessario anche un component di Dagger per istanziare un oggetto MainService e eseguire l’inject di una Activity:

@Singleton
@Component(modules = MyModule.class)
public interface MyComponent {
    MainService mainService();

    void inject(MainActivity mainActivity);
}

Test JUnit con Mockito

Usando Mockito è semplice testare la classe MainService in isolamento:

public class MainServiceTest {

    @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Mock RestService restService;

    @Mock MyPrinter myPrinter;

    @InjectMocks MainService mainService;

    @Test public void testDoSomething() {
        when(restService.getSomething()).thenReturn("abc");

        mainService.doSomething();

        verify(myPrinter).print("ABC");
    }
}

L’utilizzo di un oggetto di tipo MockitoRule è equivalente a quello di un MockitoJUnitRunner, entrambi invocano il metodo statico MockitoAnnotations.initMocks per popolare i field annotati. Grazie all’annotazione InjectMocks il campo mainService è creato automaticamente, i due mock definiti nel test sono usati come argomenti del costruttore.

Dagger non è usato in questo tipo di test, questo aspetto può essere positivo in quanto il test è un molto semplice ed è un vero unit test.

Test con Dagger 2

Alcune volte può essere necessario utilizzare un test di più alto livello che utilizza Dagger per istanziare gli oggetti. Il modo più semplice per sovrascrivere un oggetto gestito da Dagger è spiegato in questo post scritto da Artem Zinnatullin. Seguendo i suoi suggerimenti è possibile definire un TestModule che estende il modulo originale e sovrascrive i metodi per ritornare due mock:

public class TestModule extends MyModule {
    @Override public MyPrinter provideMyPrinter() {
        return Mockito.mock(MyPrinter.class);
    }

    @Override public RestService provideRestService() {
        return Mockito.mock(RestService.class);
    }
}

E’ necessario anche un TestComponent per eseguire l’injection del test:

@Singleton
@Component(modules = MyModule.class)
public interface TestComponent extends MyComponent {
    void inject(MainServiceDaggerTest test);
}

Il test contiene tre campi annotati con Inject, nel metodo setUp creiamo il TestComponent e lo usiamo per eseguire l’injection del test per popolare i tre campi:

public class MainServiceDaggerTest {

    @Inject RestService restService;

    @Inject MyPrinter myPrinter;

    @Inject MainService mainService;

    @Before public void setUp() {
        TestComponent component = DaggerTestComponent.builder()
            .myModule(new TestModule()).build();
        component.inject(this);
    }

    @Test public void testDoSomething() {
        when(restService.getSomething()).thenReturn("abc");

        mainService.doSomething();

        verify(myPrinter).print("ABC");
    }
}

Questo test funziona correttamente ma ci sono alcuni aspetti che possono essere migliorati:

  • i campi restService e myPrinter contengono due mock ma sono annoati con Inject e non con Mock come nell’esempio precedente;
  • è necessario scrivere un modulo di test e un componente di test.

DaggerMock: una regola JUnit per sovrascrivere oggetti gestiti da Dagger 2

Dagger usa un annotation processor per analizzare tutte le classi nel progetto alla ricerca di annotation, ma il TestModule del precedente esempio non contiene nessuna annotation di Dagger!

L’idea di base di DaggerMock è quella do utilizzare una JUnit rule che dinamicamente creare una sottoclasse del modulo.I metodi di questa sottoclasse del modulo ritornano i mock definiti nel test. Non è semplice da spiegare l’implementazione interna ma l’utilizzo sono solo due righe di codice:

public class MainServiceTest {

    @Rule public DaggerMockRule<MyComponent> mockitoRule = 
      new DaggerMockRule<>(MyComponent.class, new MyModule())
        .set(component -> mainService = component.mainService());

    @Mock RestService restService;

    @Mock MyPrinter myPrinter;

    MainService mainService;

    @Test
    public void testDoSomething() {
        when(restService.getSomething()).thenReturn("abc");

        mainService.doSomething();

        verify(myPrinter).print("ABC");
    }
}

In questo esempio la rule crea dinamicamente una sottoclasse di MyModule che ritorna i mock definiti nel test al posto degli oggetti reali. Questo test è simile al primo test visto in questo post (quello che usa l’annotation InjectMocks), la differenza principale è che adesso stiamo creando l’oggetto mainService usando Dagger. Altri benefici dell’utilizzo di una rule DaggerMock sono:

  • non tutte le dipendenze dell’oggetto devono essere definite nel test. Gli oggetti definiti nella configurazione di Dagger sono usati quando un oggetto non è definito nel test;
  • è semplice sovrascrivere un oggetto che non è usato direttamente (per esempio quando un oggetto A utilizza un oggetto B che a sua volta utilizza un oggetto C e vogliamo sovrascrivere solo l’oggetto C).

Test con Espresso

Ci sono molti post che parlano dell’utilizzo di Dagger e Mockito in un test sviluppato con Espresso. Per esempio questo post scritto da Chiu-Ki Chan contiene la soluzione più usata per risolvere questo problema.

Vediamo un altro esempio, una Activity che invoca il metodo del precedente esempio:

public class MainActivity extends AppCompatActivity {

    @Inject MainService mainService;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        App app = (App) getApplication();
        app.getComponent().inject(this);

        mainService.doSomething();
        //...
    }
}

E’ possibile testare questa Activity usando una ActivityTestRule, il test è simile al test MainServiceDaggerTest (un TestComponene e un TestModule sono utilizzati):

public class MainActivityTest {

    @Rule public ActivityTestRule<MainActivity> activityRule = 
      new ActivityTestRule<>(MainActivity.class, false, false);

    @Inject RestService restService;

    @Inject MyPrinter myPrinter;

    @Before
    public void setUp() throws Exception {
        EspressoTestComponent component = 
          DaggerEspressoTestComponent.builder()
            .myModule(new EspressoTestModule()).build();

        getApp().setComponent(component); 
          
        component.inject(this);
    }
    private App getApp() {
        return (App) InstrumentationRegistry.getInstrumentation()
          .getTargetContext().getApplicationContext();
    }
    @Test
    public void testCreateActivity() {
        when(restService.getSomething()).thenReturn("abc");

        activityRule.launchActivity(null);

        verify(myPrinter).print("ABC");
    }
}

DaggerMock e Espresso

Questo test può essere semplificato usando una DaggerMockRule, nella lambda expression viene impostato il component nella Application per eseguire l’override degli oggetti usando i mock:

public class MainActivityTest {

    @Rule public DaggerMockRule<MyComponent> daggerRule = 
       new DaggerMockRule<>(MyComponent.class, new MyModule())
         .set(component -> getApp().setComponent(component));
    @Rule public ActivityTestRule<MainActivity> activityRule = 
      new ActivityTestRule<>(MainActivity.class, false, false);

    @Mock RestService restService;

    @Mock MyPrinter myPrinter;
    //...
}

La rule può essere usata anche in un test gestito con Robolectric, un esempio è disponibile nel repository del progetto.

Rule JUnit custom

La stessa rule è spesso usata in tutti i test di un progetto, una sottoclasse della rule può essere utilizzata per evitare copia incolla. Per esempio la rule dell’esempio precedente può essere scritta in una nuova classe MyRule:

public class MyRule extends DaggerMockRule<MyComponent> {
    public MyRule() {
        super(MyComponent.class, new MyModule());
        set(component -> getApp().setComponent(component));
    }

    private App getApp() {
        return (App) InstrumentationRegistry.getInstrumentation()
          .getTargetContext().getApplicationContext();
    }
}

In alcuni casi è necessario sovrascrivere un oggetto che poi non è usato nel test. Per esempio in un test con Espresso solitamente non vogliamo eseguire il tracking degli eventi di analytics e inviarli a un server remoto, un mock può essere di aiuto anche in questi casi. Per definire un oggetto custom è possibile utilizzare uno dei seguenti metodi della rule:

  • provides(Class originalClass, T newObject): sovrascrive gli oggetti di una classe con uno specifico oggetto;
  • provides(Class originalClass, Provider provider): simile al precedente metodo ma utile per gli oggetti non definiti come singleton;
  • providesMock(Class<?>… originalClasses): sovrascrive usando un mock tutti gli oggetti delle classi passate come argomenti. E’ equivalente a provide(MyObject.class, Mockito.mock(MyObject.class)).

Un esempio di una custom rule che usa questi metodi è disponibile nella app di CoseNonJaviste (è sul Play Store ed è open source ed è disponibile su GitHub):

public class CnjDaggerRule 
        extends DaggerMockRule<ApplicationComponent> {
    public CnjDaggerRule() {
        super(ApplicationComponent.class, new AppModule(getApp()));
        provides(SchedulerManager.class, 
            new EspressoSchedulerManager());
        providesMock(WordPressService.class, TwitterService.class);
        set(component -> getApp().setComponent(component));
    }

    public static CoseNonJavisteApp getApp() {
        return (CoseNonJavisteApp) 
            InstrumentationRegistry.getInstrumentation()
            .getTargetContext().getApplicationContext();
    }
}

La versione finale del test di Espresso è molto semplice (e non è necessario scrivere un TestComponent o un TestModule!):

public class MainActivityTest {

    @Rule public MyRule daggerRule = new MyRule();

    @Rule public ActivityTestRule<MainActivity> activityRule = 
      new ActivityTestRule<>(MainActivity.class, false, false);

    @Mock RestService restService;

    @Mock MyPrinter myPrinter;

    @Test
    public void testCreateActivity() {
        when(restService.getSomething()).thenReturn("abc");

        activityRule.launchActivity(null);

        verify(myPrinter).print("ABC");
    }
}

DaggerMock è un progetto open source disponibile su GitHub, è integrabile facilmente in un progetto usando il repository JitPack.

Fabio Collini

Software Architect con esperienza su piattaforma J2EE e attualmente focalizzato principalmente in progetti di sviluppo di applicazioni Android. Attualmente sono nel team Android di Cynny, ci stiamo occupando dello sviluppo dell'app Morphcast. Coautore della seconda edizione di Android Programmazione Avanzata e docente di corsi di sviluppo su piattaforma Android. Follow me on Twitter - LinkedIn profile - Google+