Android Testing with Robolectric and Dagger

2020-04-20 13:09发布

I am trying to write an Android application using Dagger. Trying to follow the TDD approach, I started writing a test for my First activity. For writing tests I am using Robolectric and I am trying to make it work in different scenarios using Mockito.

Short story:

I have an android activity which I want to test using robolectric. This activity has some of its dependencies provided through Dagger. I managed to make this work by overriding the Application class and providing a mock of the utility class. What I need now is to be able to change the behavior of the utility class(using Mockito) in the same unit test file to test different scenarios.

Long story:

Development and testing environment: Android Studio 0.8.6, gradle 0.12, dagger 1.2.2, robolectric 2.3

Base Application class:

public class MyApplication extends DaggerApplication
{

@Override
protected List<Object> getAppModules() {
    List<Object> modules = new ArrayList<Object>();
    modules.add(new AppModule(this));
    return modules;
}
}

DaggerApplication class:

public abstract class DaggerApplication extends Application {

private ObjectGraph mObjectGraph;

@Override
public void onCreate() {
    super.onCreate();

    AndroidAppModule sharedAppModule = new AndroidAppModule(this);

    List<Object> modules = new ArrayList<Object>();
    modules.add(sharedAppModule);
    modules.addAll(getAppModules());

    mObjectGraph = ObjectGraph.create(modules.toArray());
}

protected abstract List<Object> getAppModules();

@Override
public void inject(Object object) {
    mObjectGraph.inject(object);
}

@Override
public ObjectGraph getObjectGraph() {
    return mObjectGraph;
}
}

Test Application:

public class TestMyApplication extends MyApplication{

@Override
protected List<Object> getAppModules() {
    List<Object> modules = super.getAppModules();
    modules.add(new GeneralUtilsModuleNoInternetConnection());
    return modules;
}    

public static <T> void injectMocks(T object) {
    CursuriDeSchimbApplication app = (TestCursuriDeSchimbApplication) Robolectric.application;
    app.inject(object);
}
}

AppModule class:

@Module(
    injects = {
            SplashScreenActivity.class
    },
    includes = AndroidAppModule.class
)
public class AppModule {

private Context app;

public AppModule()
{

}

public AppModule(Context app) {
    this.app = app;
}

@Provides
@Singleton
GeneralUtils provideGeneralUtils() {
    return new GeneralUtils();
}
}

Test Module class:

@Module(
    includes = AppModule.class,
    injects = {SplashScreenActivityTest.class,
            SplashScreenActivity.class},
    overrides = true
)
public class GeneralUtilsModuleNoInternetConnection
{
public GeneralUtilsModuleNoInternetConnection() {
}

@Provides
@Singleton
GeneralUtils provideGeneralUtils() {

    GeneralUtils mockGeneralUtils = Mockito.mock(GeneralUtils.class);

    when(mockGeneralUtils.isInternetConnection()).thenReturn(false);

    return mockGeneralUtils;
}
}

Test class:

@RunWith(RobolectricTestRunner.class)
public class SplashScreenActivityTest
{
SplashScreenActivity activity;

@Before
public void setUp()
{
    activity = Robolectric.buildActivity(SplashScreenActivity.class).create().get();
}


@Test
public void testOnCreate_whenNoInternetConnection()
{
   <!-- Here I want GeneralUtils to return false when asking for internet connection -->
}
@Test
public void testOnCreate_whenThereIsInternetConnection()
{
   <!-- Here I want GeneralUtils to return true when asking for internet connection -->
}

}

If you need more information please do ask. To summarize: I would like to know how to use different test dagger modules in the same test class for different test scenarios.

Thank you.

3条回答
神经病院院长
2楼-- · 2020-04-20 13:40

Ok, first off, user2511882 I have tried your solution before posting the question but the thing is, if you look at the structure of TestMyApplication, where I inject the test module, you would see that your suggestion and my previous tries could not work.

After rethinking the whole problem I have found a solution along the lines of my initial tries and also a more useful solution (as far as I can see it). First off, I do not rely on the TestMyApplication class anymore. Furthermore I had to do some changes to MyApplication class to make it more "test friendly" (without changing its functionality). So MyApplication class looks like this:

public class MyApplication extends DaggerApplication
{
   private List<Object> modules;
   public MyApplication() {
       modules = new ArrayList<Object>();
       modules.add(new AppModule(this));
   }

@Override
protected List<Object> getAppModules() {
    return modules;
}
}

Now I can create the two test modules, one in which I set the behavior to return true when asking for an internet connection and one which will return false for the same query.

Now, in my test class I would have the following:

@RunWith(RobolectricTestRunner.class)
public class SplashScreenActivityTest
{
    SplashScreenActivity activity;

    public void setUpNoInternet()
    {
// Now I can add the new test module to the application modules to override the real one in the application onCreate() method
        ((MyApplication)Robolectric.application).getAppModules().add(new GeneralUtilsModuleNoInternetConnection());
        activity = Robolectric.buildActivity(SplashScreenActivity.class).create().get();
    }
    public void setUpWithInternet()
    {
        ((MyApplication)Robolectric.application).getAppModules().add(new GeneralUtilsModuleWithInternetConnection());
        activity = Robolectric.buildActivity(SplashScreenActivity.class).create().get();
    }


    @Test
    public void testOnCreate_whenNoInternetConnection()
    {
        setUpNoInternet();
       <!-- Assertions -->
    }
    @Test
    public void testOnCreate_whenThereIsInternetConnection()
    {
        setUpWithInternet();
       <!-- Assertions -->
    }

}

This works fine and is along the lines of my initial plan of testing. But I think there is a more elegant solution instead of creating a new test module for each situation. The modified test module looks like this:

@Module(
    includes = AppModule.class,
    injects = {SplashScreenActivityTest.class,
            SplashScreenActivity.class},
    overrides = true
)
public class GeneralUtilsModuleTest
{
    private  GeneralUtils mockGeneralUtils;

    public GeneralUtilsModuleTest() {
        mockGeneralUtils = Mockito.mock(GeneralUtils.class);
    }

    @Provides
    @Singleton
    GeneralUtils provideGeneralUtils() {

        return mockGeneralUtils;
    }

    public GeneralUtils getGeneralUtils()
    {
        return mockGeneralUtils;
    }

    public void setGeneralUtils(final GeneralUtils generalUtils)
    {
        this.mockGeneralUtils = generalUtils;
    }
}

Using this, the Test class looks like this:

    @RunWith(RobolectricTestRunner.class)
public class SplashScreenActivityTest
{
    SplashScreenActivity activity;

    private GeneralUtilsModuleTest testModule;
    private GeneralUtils generalUtils;

    @Before
    public void setUp()
    {
        testModule = new GeneralUtilsModuleTest();
        generalUtils = Mockito.mock(GeneralUtils.class);
    }

    public void setUpNoInternet()
    {
        when(generalUtils.isInternetConnection()).thenReturn(false);
        testModule.setGeneralUtils(generalUtils);
        ((MyApplication)Robolectric.application).getAppModules().add(testModule);
        activity = Robolectric.buildActivity(SplashScreenActivity.class).create().get();
    }
    public void setUpWithInternet()
    {
        when(generalUtils.isInternetConnection()).thenReturn(true);
        testModule.setGeneralUtils(generalUtils);
        (MyApplication)Robolectric.application).getAppModules().add(testModule);
        activity = Robolectric.buildActivity(SplashScreenActivity.class).create().get();
    }
    .....(Tests)....
}

Thank you all for your help and I really hope that this solution will help others achieve better testing on Android.

查看更多
迷人小祖宗
3楼-- · 2020-04-20 13:42

Seems like you're looking for module override (like Roboguice does). I couldn't find any, but in my tests, I've been using something like this:

MyObjectTest.java

@Test
public void testMyObject() {
    ObjectGraph objectGraph = ObjectGraph.create(new TestModule());
    MyObject object = objectGraph.get(MyObject.class);
    assertNotNull(object);
    assertEquals("Received message from MyObjectTestImpl", object.getMessage());
}

TestModule.java

public class TestModule {
    @Provides
    public Library provideMyObject() {
        return new MyObjectTestImpl();
    }
}

If MyObject is used in an Activity, I can also test it:

@RunWith(RoboGradleTestRunner.class)
public class RoboTest {
    @Test
    public void testTextView() {
        MainActivity activity = (MainActivity) Robolectric.buildActivity(MainActivity.class).create().get();

        assertEquals("Received message from MyObjectTestImpl", activity.getMyObject().getMessage());
    }
}
查看更多
姐就是有狂的资本
4楼-- · 2020-04-20 13:54

Here's what you can do. Create two different modules in the test class. One which provides Internet Connection as true and another as Internet Connection as False. Once you have the two different module's setup inject them in the individual test class rather than the setUp of the Test Class. So:

@Module(
    includes = AppModule.class,
    injects = {SplashScreenActivityTest.class,
            SplashScreenActivity.class},
    overrides = true
)
public class GeneralUtilsModuleNoInternetConnection
{
public GeneralUtilsModuleNoInternetConnection() {
}

@Provides
@Singleton
GeneralUtils provideGeneralUtils() {

    GeneralUtils mockGeneralUtils = Mockito.mock(GeneralUtils.class);

    when(mockGeneralUtils.isInternetConnection()).thenReturn(false);

    return mockGeneralUtils;
}
}

The second module:

@Module(
    includes = AppModule.class,
    injects = {SplashScreenActivityTest.class,
            SplashScreenActivity.class},
    overrides = true
)
public class GeneralUtilsModuleWithInternetConnection
{
public GeneralUtilsModuleNoInternetConnection() {
}

@Provides
@Singleton
GeneralUtils provideGeneralUtils() {

    GeneralUtils mockGeneralUtils = Mockito.mock(GeneralUtils.class);

    when(mockGeneralUtils.isInternetConnection()).thenReturn(true);

    return mockGeneralUtils;
}
}

And in you test class:

    @Test
    public void testOnCreate_whenNoInternetConnection()
    {
       <!-- Here You want to inject the GeneralUtilsModuleNoInternetConnection module and test it out-->
    }
    @Test
    public void testOnCreate_whenThereIsInternetConnection()
    {
       <!-- Here You want to inject the GeneralUtilsModuleWithInternetConnection module and test it out -->
    }

Since you are injecting the modules in the test class itself, their scope is just local and you should be just fine.

Another way is you might just inject one of the modules in setUp. Use it across all the test cases. And just for the test that you need internet connection, inject the GeneralUtilsModuleWithInternetConnection in the test itself.

Hope this helps.

查看更多
登录 后发表回答