Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 = DaggerTestMyApplication_TestNetworkComponent.builder()
                .testMyAppModule(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);
    }

}
like image 790
Donal Rafferty Avatar asked Jul 13 '16 19:07

Donal Rafferty


2 Answers

You don't need to add setters to your TestApplication and modules. You are using Dagger 2 so you should use it to inject dependencies in your test too:

First in your MyApplication create a method to retrieve the ApplicationComponent. This method will be overrided in the TestMyApplication class:

public class MyApplication extends Application {

    private ApplicationComponent mApplicationComponent;

    public ApplicationComponent getOrCreateApplicationComponent() {
        if (mApplicationComponent == null) {
            mApplicationComponent = DaggerApplicationComponent.builder()
                    .myAppModule(new MyAppModule(this))
                    .networkModule(new NetworkModule())
                    .build();
        }
        return mApplicationComponent;
    }
}

then create a TestNetworkComponent:

@Singleton
@Component(modules = {MyAppModule.class, TestNetworkModule.class})
public interface TestApplicationComponent extends ApplicationComponent {
    void inject(MainActivityTest mainActivityTest);
}

In the TestNetworkModule return a mock

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

In your TestMyApplication, build the TestNetworkComponent:

public class TestMyApplication extends MyApplication {

    private TestApplicationComponent testApplicationComponent;

    @Override
    public TestApplicationComponent getOrCreateApplicationComponent() {
        if (testApplicationComponent == null) {
            testApplicationComponent = DaggerTestApplicationComponent
                    .builder()
                    .myAppModule(new MyAppModule(this))
                    .testNetworkModule(new TestNetworkModule())
                    .build();
        }
        return testApplicationComponent;
    }
}

then in your MainActivityTest run with the application tag and inject your dependency:

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, application = TestMyApplication.class)
public class MainActivityTest {

    @Inject
    Picasso picasso;

    @Before
    public void setup() {
        ((TestMyApplication)RuntimeEnvironment.application).getOrCreateApplicationComponent().inject(this);
        Mockito.when(picasso.load(Matchers.anyString())).thenReturn(Mockito.mock(RequestCreator.class));
    }


    @Test
    public void test() {
        Robolectric.buildActivity(MainActivity.class).create();
    }

}

Your Picasso field has been injected with your Picasso mock now you can interact with it.

like image 161
Steve C Avatar answered Nov 12 '22 03:11

Steve C


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.

like image 38
jbarat Avatar answered Nov 12 '22 02:11

jbarat