Android Unit Tests with Dagger 2

JavaAndroidUnit TestingDagger 2

Java Problem Overview


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);
    }
}

Java Solutions


Solution 1 - Java

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:

Solution 2 - Java

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.

Solution 3 - Java

In my opinion you can approach this problem by looking at it from a different angle. You will easily be able to unit test your class by not depending upon Dagger for construction class under test with its mocked dependencies injected into it.

What I mean to say is that in the test setup you can:

  • Mock the dependencies of the class under test
  • Construct the class under test manually using the mocked dependencies

We don't need to test whether dependencies are getting injected correctly as Dagger verifies the correctness of the dependency graph during compilation. So any such errors will be reported by failure of compilation. And that is why manual creation of class under test in the setup method should be acceptable.

Code example where dependency is injected using constructor in the class under test:

public class BoardModelTest {

  private BoardModel boardModel;
  private Random random;

  @Before
  public void setUp() {
    random = mock(Random.class);
    boardModel = new BoardModel(random);
  }

  @Test
  ...
}

public class BoardModel {
  private Random random;

  @Inject
  public BoardModel(Random random) {
    this.random = random;
  }

  ...
}

Code example where dependency is injected using field in the class under test (in case BoardModel is constructed by a framework):

public class BoardModelTest {

  private BoardModel boardModel;
  private Random random;

  @Before
  public void setUp() {
    random = mock(Random.class);
    boardModel = new BoardModel();
    boardModel.random = random;
  }

  @Test
  ...
}

public class BoardModel {
  @Inject
  Random random;
  
  public BoardModel() {}
  
  ...
}

Solution 4 - Java

If you are using dagger2 with Android, you can use app flavours for providing mocking resources.

See here for a demo of flavours in mock testing(without dagger): https://www.youtube.com/watch?v=vdasFFfXKOY

This codebase has an example: https://github.com/googlecodelabs/android-testing

In your /src/prod/com/yourcompany/Component.java you provide your production components.

In your /src/mock/com/yourcompany/Component.java you provide your mocking components.

This allows you create builds of your app with or without mocking. It also allows parallel development (backend by one team, frontend app by another team), you can mock until api methods are avilable.

How my gradle commands look (its a Makefile):

install_mock:
    ./gradlew installMockDebug

install:
    ./gradlew installProdDebug

test_unit:
    ./gradlew testMockDebugUnitTest

test_integration_mock:
    ./gradlew connectedMockDebugAndroidTest

test_integration_prod:
    ./gradlew connectedProdDebugAndroidTest

Solution 5 - Java

I actually had the same issue and found a very simple solution. This is not the best possible solution I think but it will solve your problem.

Create a similar class in your app module:

public class ActivityTest<T extends ViewModelBase> {

    @Inject
    public T vm;
}

Then, in your AppComponent add:

void inject(ActivityTest<LoginFragmentVM> activityTest);

Then you will be able to inject that in your test class.

 public class HelloWorldEspressoTest extends ActivityTest<LoginFragmentVM> {

    @Rule
    public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule(MainActivity.class);

    @Test
    public void listGoesOverTheFold() throws InterruptedException {
        App.getComponent().inject(this);
        vm.email.set("1234");
        closeSoftKeyboard();
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPikalingView Question on Stackoverflow
Solution 1 - JavatomrozbView Answer on Stackoverflow
Solution 2 - JavaIgorGanapolskyView Answer on Stackoverflow
Solution 3 - JavaPraveer GuptaView Answer on Stackoverflow
Solution 4 - JavaTjerkWView Answer on Stackoverflow
Solution 5 - JavaRoy Ben ShabatView Answer on Stackoverflow