Android Unit Tests with Dagger 2

I have an Android app that uses Dagger 2 for dependency injection. I am also using the latest gradle build tools that allow a build variant for unit testing and one for instrumentation tests. I am using java.util.Random in my app, and I want to mock this for testing. The classes I'm testing don't use any Android stuff, so they're just regular java classes.

In my main code I define a Component in a class that extends the Application class, but in the unit tests I'm not using an Application. I tried defining a test Module and Component, but Dagger won't generate the Component. I have also tried using the Component that I defined in my application and swapping the Module when I build it, but the application's Component doesn't have inject methods for my test classes. How can I provide a mock implementation of Random for testing?

Here's some sample code:

Application:

public class PipeGameApplication extends Application {

    private PipeGame pipeGame;

    @Singleton
    @Component(modules = PipeGameModule.class)
    public interface PipeGame {
        void inject(BoardFragment boardFragment);
        void inject(ConveyorFragment conveyorFragment);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        pipeGame = DaggerPipeGameApplication_PipeGame.create();
    }

    public PipeGame component() {
        return pipeGame;
    }
}

Module:

@Module
public class PipeGameModule {

    @Provides
    @Singleton
    Random provideRandom() {
        return new Random();
    }
}

Base class for tests:

public class BaseModelTest {

    PipeGameTest pipeGameTest;

    @Singleton
    @Component(modules = PipeGameTestModule.class)
    public interface PipeGameTest {
        void inject(BoardModelTest boardModelTest);
        void inject(ConveyorModelTest conveyorModelTest);
    }

    @Before
    public void setUp() {
        pipeGameTest = DaggerBaseModelTest_PipeGameTest.create(); // Doesn't work
    }

    public PipeGameTest component() {
        return pipeGameTest;
    }
}

or:

public class BaseModelTest {

    PipeGameApplication.PipeGame pipeGameTest;

    // This works if I make the test module extend
    // the prod module, but it can't inject my test classes
    @Before
    public void setUp() {
        pipeGameTest = DaggerPipeGameApplication_PipeGame.builder().pipeGameModule(new PipeGameModuleTest()).build();
    }

    public PipeGameApplication.PipeGame component() {
        return pipeGameTest;
    }
}

Test Module:

@Module
public class PipeGameTestModule {

    @Provides
    @Singleton
    Random provideRandom() {
        return mock(Random.class);
    }
}

Solution 1:

This is currently impossible with Dagger 2 (as of v2.0.0) without some workarounds. You can read about it here.

More about possible workarounds:

  • How do you override a module/dependency in a unit test with Dagger 2.0?

  • Creating test dependencies when using Dagger2

Solution 2:

You have hit the nail on the head by saying:

application's Component doesn't have inject methods for my test classes

So, to get around this problem we can make a test version of your Application class. Then we can have a test version of your module. And to make it all run in a test, we can use Robolectric.

1) Create the test version of your Application class

public class TestPipeGameApp extends PipeGameApp {
    private PipeGameModule pipeGameModule;

    @Override protected PipeGameModule getPipeGameModule() {
        if (pipeGameModule == null) {
            return super.pipeGameModule();
        }
        return pipeGameModule;
    }

    public void setPipeGameModule(PipeGameModule pipeGameModule) {
        this.pipeGameModule = pipeGameModule;
        initComponent();
    }}

2) Your original Application class needs to have initComponent() and pipeGameModule() methods

public class PipeGameApp extends Application {
    protected void initComponent() {
        DaggerPipeGameComponent.builder()
            .pipeGameModule(getPipeGameModule())
            .build();
    }

    protected PipeGameModule pipeGameModule() {
        return new PipeGameModule(this);
    }}

3) Your PipeGameTestModule should extend the production module with a constructor:

public class PipeGameTestModule extends PipeGameModule {
    public PipeGameTestModule(Application app) {
        super(app);
    }}

4) Now, in your junit test's setup() method, set this test module on your test app:

@Before
public void setup() {
    TestPipeGameApp app = (TestPipeGameApp) RuntimeEnvironment.application;
    PipeGameTestModule module = new PipeGameTestModule(app);
    app.setPipeGameModule(module);
}

Now you can customize your test module how you originally wanted.