JakeWharton/u2020

Provide examples for unit tests

mttkay opened this issue · 6 comments

I struggle to find a good reference for how Dagger is used to inject dependencies in unit tests, especially when fragments and activities are involved.

I believe we're at our 3rd round of rewriting this in our app (we finally moved away completely from the clunky and verbose approach where you inject into the test class using a test module), and would be interested to see what Square suggests here.

FWIW:
We went back to cutting out Dagger from unit tests and having @VisibleForTesting constructors on fragments, and then pass in mocks manually in unit tests. In the default ctor, the one Android would invoke, we then inject the object graph and have package visible fields with @Inject annotations. That introduces a weird split though between how dependencies are resolved (manually in tests, via Dagger in prod.)

@mttkay I wouldn't disrupt your production code just to satisfy unit testing. Where were you using Dagger to inject your dependencies previously?

I spent quite a bit of time figuring this out, and I think I might have something useful for you (if you're using Robolectric as well).

None of our unit tests involve activities or fragments. I'm extremely ill-equipped to advise on the latter since we don't use fragments. We unit test the object which are injected into the activities and fragments which almost never requires knowledge of injection since they can all be instantiated with constructor injection.

There's not much worth testing in this teeny throwaway app, unfortunately. The only thing that really does anything is the GalleryDatabase. If we wanted to test it I'd switch the two Scheduler instances to be injected in the constructor and then just set up the GalleryDatabaseTest something like this:

public class GalleryDatabaseTest {
  // ..

  @Before public void setUp() {
    bg = new TestScheduler();
    fg = new TestScheduler();
    gs = mock(GalleryService.class);
    gd = new GalleryDatabase(gs, fd, bg);

    doReturn(new Gallery(200, true, Collection.emptyList())) //
        .when(gs).loadGallery(any(), any(), anyInt());
  }

  @Test public void twoSubscribersJoinSingleHttpRequest() {
    Observer o = new EmptyObserver();
    gb.loadGallery(o);
    gb.loadGallery(o);
    verify(gs).loadGallery(any(), any(), anyInt());
    verifyNoMoreInteractions(gs);
  }
}

If you had a more complex object hierarchy and absolutely needed Dagger you could create a module as a static-inner class of the test which provided everything needed and use that.

Since Dagger requires field injections to be package scoped or higher you can also just assign them directly. For example, a view:

@Test public void viewDoesSomething() {
  MyView v = new MyView(someContext, null);
  v.dependencyOne = mock(One.class);
  v.dependencyTwo = mock(Two.class);

  v.doSomething();
  assertThat(v.otherThing()).isEqualTo(42);
}

I'd probably have much more to say if Gradle had first-party unit test support and if this app contained a bit more logic.

@JakeWharton Where in your View are you doing ((Injector)context).inject(this);? The canonical Android custom View example usually does all the setup in the constructor call. I've been using onAttachedToWindow().

In the constructor.

public class TestContext extends ContextWrapper implements Injector {
  public TestContext(Context context) {
    super(context);
  }

  @Override public void inject(Object o) {}
}

Wrap Robolectric.application in an TestContext and you should be good to go.

Robolectric.application starts up the world (I'm doing quite a bit of setup in my Application). Do we really need to do this for a View test?

I have no idea. Depends on whatever you are doing.