Donal Rafferty Donal Rafferty - 4 months ago 75
Java Question

How to test with Dagger2 Dependency Injection & Robolectric in Android?

I recently implemented Dagger2 into an Android application for easy dependency injection but after doing so some of my tests have stopped working.

Now I am trying to understand how to adjust my tests to work with Dagger2? I am using Robolectric for running my tests.

Here is how I use Dagger2, I have only recently learned it so this may be bad practice and not helping the tests so please do point out any improvements I can make.

I have an AppModule which is as follows:

@Module
public class MyAppModule {

//Application reference
Application mApplication;

//Set the application value
public MyAppModule(Application application) {
mApplication = application;
}

//Provide a singleton for injection
@Provides
@Singleton
Application providesApplication() {
return mApplication;
}
}


And what I call a NetworkModule that provides the objects for injection that is as follows:

@Module
public class NetworkModule {

private Context mContext;

//Constructor that takes in the required context and shared preferences objects
public NetworkModule(Context context){
mContext = context;
}

@Provides
@Singleton
SharedPreferences provideSharedPreferences(){
//...
}

@Provides @Singleton
OkHttpClient provideOKHttpClient(){
//...
}

@Provides @Singleton
Picasso providePicasso(){
//...
}

@Provides @Singleton
Gson provideGson(){
//...
}
}


And then the Component is like this:

Singleton
@Component(modules={MyAppModule.class, NetworkModule.class})
public interface NetworkComponent {

//Activities that the providers can be injected into
void inject(MainActivity activity);
//...
}


For my tests I am using Robolectric, and I have a Test variant of my Application class as follows:

public class TestMyApplication extends TestApplication {

private static TestMyApplication sInstance;
private NetworkComponent mNetworkComponent;

@Override
public void onCreate() {
super.onCreate();
sInstance = this;
mNetworkComponent = DaggerTestCompanionApplication_TestNetworkComponent.builder()
.testCompanionAppModule(new TestMyAppModule(this))
.testNetworkModule(new TestNetworkModule(this)).build();
}

public static MyApplication getInstance() {
return sInstance;
}

@Override public NetworkComponent getNetComponent() {
return mNetworkComponent;
}
}


As you can see I am trying to make sure the mocked versions of my Dagger2 Modules are used, these are mocked as well with the mocked MyAppModule returning the TestMyApplication and the mocked NetworkModule returning mocked objects, I also have a mocked NetworkComponent which extends the real NetworkComponent.

In the setup of a test I create the Activity using Robolectric like this:

//Build activity using Robolectric
ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
activity = controller.get();

controller.create(); //Create out Activity


This creates the Activity and starts the onCreate, and this is where the issue occurs, in the onCreate I have the following piece of code to inject the Activity into the component so it can use Dagger2 like this:

@Inject Picasso picasso; //Injected at top of Activity

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
MyApplication.getInstance().getNetComponent().inject(this);

picasso.load(url).fetch();


The problem here is that when running the test I get a NullPointerException on the picasso variable, so I guess my Dagger2 setup has a missing link somewhere for the tests?

EDIT: Adding TestNetworkModule

@Module
public class TestNetworkModule {

public TestNetworkModule(Context context){

}

@Provides
@Singleton
SharedPreferences provideSharedPreferences(){
return Mockito.mock(SharedPreferences.class);
}


@Provides @Singleton
Gson provideGson(){
return Mockito.mock(Gson.class);
}

@Provides @Singleton
OkHttpClient provideOKHttpClient(){
return Mockito.mock(OkHttpClient.class);
}

@Provides @Singleton
Picasso providePicasso(){
return Mockito.mock(Picasso.class);
}

}

Answer

Just giving back mocks are not enough. You need to instruct your mocks what they should return for different calls.

I'm giving you an example for just the Picasso mock, but it should be similar for all. I'm writing this on the Tube, so treat this as pseudo code.

Change your TestMyApplication so you can set the modules from outside something like this:

public class TestMyApplication extends TestApplication {

    private static TestMyApplication sInstance;
    private NetworkComponent mNetworkComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        sInstance = this;
    }

    public void setModules(MyAppModule applicationModule, NetworkModule networkModule) {
        this.applicationModule = applicationModule;
        this.mNetworkComponent = DaggerApplicationComponent.builder()
                .applicationModule(applicationModule)
                .domainModule(networkModule)
                .build();
    }

    public static MyApplication getInstance() {
        return sInstance;
    }

    @Override public NetworkComponent getNetComponent() {
        return mNetworkComponent;
    }
}

Now you can control your modules from the tests.


Next step make your mocks accesable. Something like this:

@Module
public class TestNetworkModule {

    private Picasso picassoMock;

    ...

    @Provides @Singleton
    Picasso providePicasso(){
        return picassoMock;
    }

    public void setPicasso(Picasso picasso){
        this.picasso = picasso;
    }
}

Now you can control all your mock.


Now everything is set up for testing lets make one:

@RunWith(RobolectricGradleTestRunner.class)
public class PicassoTest {

    @Mock Picasso picasso;
    @Mock RequestCreator requestCreator;

    @Before
    public void before(){
        initMocks(this);

        when(picassoMock.load(anyString())).thenReturn(requestCreator);

        TestApplication app = (TestApplication) RuntimeEnvironment.application;

        TestNetworkModule networkModule = new TestNetworkModule(app);
        networkModule.setPicasso(picasso);

        app.setModules(new TestMyAppModule(this), networkModule);
        //Build activity using Robolectric
        ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
        activity = controller.get();
        activity.create();
    }

    @Test
    public void test(){
        //the test
    }

    @Test
    public void test2(){
        //another test
    }
}

So now you can write your tests. Because the setup is in the before you don't need to do this in every test.

Comments