google / dagger

A fast dependency injector for Android and Java.
https://dagger.dev
Apache License 2.0
17.45k stars 2.02k forks source link

Testing/Overriding Modules #110

Closed chrisjenx closed 8 years ago

chrisjenx commented 9 years ago

Apologies if this is a dup (couldn't see an open issue).

Is there any standard way to do testing. Overriding @Provides or extending module classes.
I know it was discussed by Jake at Devvox that it still needs fleshing out.

What are the current thoughts? This for me is the only real blocker. I can work around the limitations, but it feels like yak-shaving.

passsy commented 9 years ago

This is a big blocker for me, too. May you share your current workaround with us? I'm currently stuck replacing Picasso with a mock.

artem-zinnatullin commented 9 years ago

You can use Gradle's flavors and write different Module implementation in required flavor(s)

svenjacobs commented 9 years ago

@artem-zinnatullin If you only want to override certain bindings, you still would have to copy the whole module with all other bindings, too. The other possibility would be to create a TestModule, which however must be an empty class in all other flavors. Last but not least, what about non-Android environments where Dagger is used?

chrisjenx commented 9 years ago

@artem-zinnatullin that is a work around, but not really using the build systems as intended. I feel there should be a more "natural" approach which plays nice with the build systems + dagger. I'm sure this has been discussed internally between the dagger team, just interested on thoughts/best practice and what direction this is going.

artem-zinnatullin commented 9 years ago

@svenjacobs @chrisjenx I understand, that this is not perfect solution.

In Dagger 1 I used module overriding + build flavors. I will work on unit tests + Dagger 2 this week and post my workaround.

@svenjacobs Gradle is not only for Android projects, we successfully use it for Java projects (backends and utils), Gradle supports many platforms, you can even build an iOS app with it.

@passsy hope you use Gradle, just create module class which provides mocked Picasso in test flavor

svenjacobs commented 9 years ago

@artem-zinnatullin I know that Gradle is a generic build system, but flavors, the solution you suggested, is Android-specific and is coming from the Android Gradle plugin. Also Dagger might be used in a project with Maven or another build system.

artem-zinnatullin commented 9 years ago

@svenjacobs yes, you are correct, build flavors is Android Gradle plugin's feature

passsy commented 9 years ago

Build flavors are not the solution. I need to inject different mock classes for different tests.

bryanstern commented 9 years ago

This may be of some use, however it is likely broken at the moment due to some snapshot changes. https://github.com/bryanstern/dagger-instrumentation-example

chrisjenx commented 9 years ago

@bryanstern As discussed above, using buildTypes, isn't really a "solution" more of a work around. But thanks for the example nonetheless.

felipecsl commented 9 years ago

+1 for this, it was possible with Dagger v1 but not anymore on v2.

stephanenicolas commented 9 years ago

This issue is important for us too. It somewhat looks like if tests where not part of the design of Dagger2, and I can't really believe that.

I currently use a work around : create a test module with a test scope, create an injector/component during test and inject the activity/entity under test with it. But it means that the activity for instance will receive twice the injection : one from production code and one for testing. It's not a very big deal but a bit awkward.


@RunWith(RobolectricTestRunner.class)
public class CarouselTest {

    @Test
    public void testCarouselWithDependency() {
        ActivityController<Carousel> carouselActivityController = Robolectric.buildActivity(Carousel.class).create();
        Carousel carousel = carouselActivityController.get();
        di.DbTestAppComponent dbTestAppComponent = ((DbTestApplication) carousel.getApplicationContext()).getComponent();
        dbTestAppComponent.inject(carousel);
        carousel.init(null); //call anything that is done after the injection in onCreate
    }

    @TestScope
    @Component(dependencies = {DbTestAppComponent.class}, modules = {TestDbTestAppModule.class})
    public interface TestDbTestAppComponent {
        void inject(Carousel carousel);
    }

    @Module(overrides = true) //that should be needed but is that implemented ?
    public class TestDbTestAppModule {
        @Provides
        public SharedPreferences getSharedPreferences() {
            return Robolectric.application.getSharedPreferences("a", 0);
        }
    }
}
felipecsl commented 9 years ago

@stephanenicolas I did something similar, but instead I created also a test component to mirror the production component, so the test classes only get injections from the test module, instead of getting twice. It sucks but works

twelve17 commented 9 years ago

@felipecsl Does your test component live in the androidTest source tree? I'm trying to do the same but a Dagger_TestComponent generated class (e.g. from the TestComponent interface) isn't being created--I'm thinking that Dagger is not looking in my androidTest source tree for components, but I'm not sure.

felipecsl commented 9 years ago

@twelve17 I'm using the new unit testing support on gradle build tools 1.1, so they are all under src/test. androidTest should have your Espresso tests instead. I also had to add the dagger dependency to testCompile on the build.gradle file like this:

testCompile 'com.google.dagger:dagger:2.0-SNAPSHOT'
testCompile 'com.google.dagger:dagger-compiler:2.0-SNAPSHOT'
twelve17 commented 9 years ago

@felipecsl thanks for the info. Alas, it turned out I had a compilation error which seemed to be preventing the Dagger class from being generated.

tomrozb commented 9 years ago

What do you guys think about this solution: https://stackoverflow.com/questions/26939340/how-do-you-override-a-module-dependency-in-a-unit-test-with-dagger-2-0/29996385#29996385

Here's sample project: https://github.com/tomrozb/dagger-testing

The proposed solution is based on extending the production components in test code. This seems as a workaround for v2.0.0.

public class App extends Application {

    private AppComponent mAppComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        mAppComponent = DaggerApp_AppComponent.create();
    }

    public AppComponent component() {
        return mAppComponent;
    }

    /**
     * Visible only for testing purposes.
     */
    // @VisibleForTesting
    public void setTestComponent(AppComponent appComponent) {
        mAppComponent = appComponent;
    }

    @Singleton
    @Component(modules = StringHolderModule.class)
    public interface AppComponent {

        void inject(MainActivity activity);
    }

    @Module
    public static class StringHolderModule {

        @Provides
        StringHolder provideString() {
            return new StringHolder("Release string");
        }
    }
}

In test code:

    @Component(modules = TestStringHolderModule.class)
    interface TestAppComponent extends AppComponent {

    }

    @Module
    static class TestStringHolderModule {

        @Provides
        StringHolder provideString() {
            return new StringHolder(TEST_STRING);
        }
    }
// Before activity is created
((App) application).setTestComponent(mTestAppComponent);
stephanenicolas commented 9 years ago

@tomrozb the problem is that you are changing your production code so that it knows about test which is generally something you try to avoid. It couples production and tests in a very bad way.

stephanenicolas commented 9 years ago

The problem indeed is that Dagger 2 requires manual creation of the component inside activities and there is no way to access the component / injector when it is created. It looks like there Dagger 2 would need one more level of indirection to create a component.

I would suggest a very hacky approach here, but that could work and would offer some advantages :

This is hacky but it would actually fit well in the Dagger 2 design, would avoid to couple testing and production code, would be side-effect free (as the module is consumed by each test entirely).

@cgruber Would do think of this approach ?

ScottPierce commented 9 years ago

This is the issue that has stopped me from migrating to Dagger 2 thus far.

trevorrjohn commented 9 years ago

@stephanenicolas am I wrong that in your example anything that happens in onCreate would be called with the real dependencies?

ActivityController<Carousel> carouselActivityController = Robolectric.buildActivity(Carousel.class).create();
Carousel carousel = carouselActivityController.get();
di.DbTestAppComponent dbTestAppComponent = ((DbTestApplication) carousel.getApplicationContext()).getComponent();
dbTestAppComponent.inject(carousel);
carousel.init(null); //call anything that is done after the injection in onCreate

This essentially moves any setup to later in the lifecycle that requires dependencies, no?

mheras commented 9 years ago

The solution from @stephanenicolas is the cleanest one, in my opinion, as you don't expose any setter on the target entities/activities to use your mocked component/module. But it's still not perfect as it's injecting twice.

This is making me avoid the migration :(

tbroyer commented 9 years ago

Best solution IMO is the one from @pyricau: https://groups.google.com/d/topic/dagger-discuss/FtbWILcoqHM

Disclaimer: I haven't yet really used Dagger, and never wrote such tests.

svenjacobs commented 9 years ago

The migration guide states:

Modules that use overrides and rely on dependency injection should be decomposed so that the overriden modules are instead represented as a choice between two modules.

An example by the Dagger developers would be nice. Even if I move the dependency which I want to replace in test scope into a separate module and create a subclass of that module it wouldn't be possible since Dagger doesn't allow to subclass modules and override provides methods. Or what is meant by "choice between two modules"?

stephanenicolas commented 9 years ago

I agree, this part of the migration docs is really not clear. I read it multiple times and couldn't get it.

2015-06-10 12:25 GMT-07:00 Sven Jacobs notifications@github.com:

The migration guide http://google.github.io/dagger/dagger-1-migration.html states:

Modules that use overrides and rely on dependency injection should be decomposed so that the overriden modules are instead represented as a choice between two modules.

An example by the Dagger developers would be nice. Even if I move the dependency which I want to replace in test scope into a separate module and create a subclass of that module it wouldn't be possible since Dagger doesn't allow to subclass modules and override provides methods. Or what is meant by "choice between two modules"?

— Reply to this email directly or view it on GitHub https://github.com/google/dagger/issues/110#issuecomment-110885265.

dalewking commented 9 years ago

I have created a little injection library as a way to break the compile time dependency on the @Module from production code to solve this very problem. Most of the examples I see for Dagger2 on Android to me are little better than directly instantiating classes in your activity, because they are compile time bound to a particular module and do not allow switching to a different module.

https://gitlab.com/NobleworksSoftware/AndroidInjectorFramework

stephanenicolas commented 9 years ago

@dalewking , you should create a sample to demonstrate how it is related to the issue.

dalewking commented 9 years ago

@stephanenicolas I know I definitely do. I need to find an existing library to fork and apply the library.

However when applying the library I created to a work project I ran into issues in the case where you have one class that extends another class that also has its own injections. My technique would not compile because both classes wanted to extend Injectable but with different type arguments so am having to rethink the solution.

I'm more and more coming back to the idea of using reflection to generate a map from class to methods on the component like in https://github.com/konmik/Dagger2Example/blob/master/app/src/main/java/info/android15/dagger2example/Dagger2Helper.java. I know as soon as you say reflection in connection with dependency injection many people will stop listening to you since they had such a bad experience with reflection in Guice on Android. But as this article shows this time needed for this little bit of reflection is negligible: https://github.com/konmik/konmik.github.io/wiki/Snorkeling-with-Dagger-2#performance. People also equate using reflection with "injection can fail at runtime", but since you want to actually allow the decision of what gets injected to be able to made at runtime then to me it is a given that injection could fail at runtime.

mheras commented 9 years ago

Hi all,

It's been a while... Did anyone come up with something to solve this issue properly?

Thank you.

dalewking commented 9 years ago

@mheras I do have a solution, but it is not quite ready for public consumption (needs readme, documentation, example, etc.)

It basically has 2 parts one is an annotation processor that generates a generic wrapper class for each component and subcomponent that lets you do injection using generic methods. It is basically my own rewrite of the injection portion of the bullet library by tbroyer (https://github.com/tbroyer/bullet). Bullet is a great concept, but I found the implementation classes that it generated totally unacceptable. I would much rather this actually be done in Dagger 2 because it would be much easier there (see https://github.com/google/dagger/issues/213)

The other half is what I described earlier, but now is greatly simplified, which basically hides injection away behind a static injection service. You create an implementation of an injector interface:

public interface Injector
{
    void inject(Context context, Object target);
}

There is a class with static methods to register the instance of the interface to use for the app and to do injection for all the injection challenged Android classes.

So in your activities, fragments, views, services, etc. you only have to add a line in the appropriate place of the form:

InjectionService.inject(this);

For unit tests, I just register a mock version of the Injector interface in my test setup code.

I will try to get it pushed and somewhat reviewable this week.

mheras commented 9 years ago

@dalewking That sounds REALLY good... Let me know if you need help testing it!

mattkranzler5 commented 9 years ago

I came up with a workaround to allow for module specific overrides which seems better than the multiple source set approach. It still seems a bit hacky though since it's kind of going against the module apis. In each of your modules where you would like to override a dependency create a method which provides the dependency and modify your Provides method to return that method. Now you can create a module which extends this module and override the method to provide a test/mock dependency. Here is an example:

public class OverriddenModules {

    @Component(modules = {
            AppModule.class,
            NetworkModule.class
    })
    public static interface AppComponent {
         void inject(Application application);
    }

    @Module
    public static class AppModule {

        @Provides
        AppDependency provideAppDependency() {
            return getAppDependency();
        }

        protected AppDependency getAppDependency() {
            return new AppDependency();
        }
    }

    @Module
    public static class NetworkModule {

        @Provides
        NetworkDependency provideNetworkDependency() {
            return getNetworkDependency();
        }

        protected NetworkDependency getNetworkDependency() {
            return new NetworkDependency();
        }
    }

    @Component(modules = {
            TestApplicationModule.class,
            TestNetworkModule.class
    })
    public static interface TestAppComponent extends AppComponent {
         void inject(TestApplication testApplication);
    }

    @Module
    public static class TestAppModule extends AppModule {

        @Override
        protected AppDependency getAppDependency() {
            return Mockito.mock(AppDependency.class);
        }
    }

    @Module
    public static class TestNetworkModule extends NetworkModule {

        @Override
        protected NetworkDependency getNetworkDependency() {
            return Mockito.mock(NetworkDependency.class);
        }
    }
}
JorisPotier commented 9 years ago

@mattkranzler5 I am still getting the message "@Provides methods may not override another method." when I try your solution. Any idea ?

mattkranzler5 commented 9 years ago

@JorisPotier did you make sure to override your method that is NOT marked @Provides? In your subclass module you cannot override your provides methods, only the delegate method, as shown above.

JorisPotier commented 9 years ago

@mattkranzler5 :+1: it works better now thanks

JorisPotier commented 9 years ago

except that I add to modify your TestAppComponent like this, because I add some build errors in my case, telling me that I did not implement some Providers for some dependencies.

@Component(modules = {
            ApplicationModule.class,
            NetworkModule.class
})
public static interface TestAppComponent extends AppComponent {

}
mattkranzler5 commented 9 years ago

Hmm... that shouldn't be necessary. If you do that the test component won't have the proper test module composition. As long as the modules defined in the test component are subclasses of the modules defined in the regular component it should work.

tbroyer commented 9 years ago

@mattkranzler5 You don't have to create a specific test component, just pass your test modules in explicitly (using builder().applicationModule(new TestApplicationModule())) rather than letting Dagger create them for you (e.g. when using create()); and that way you don't have to create new methods an can just override the @Provides methods.

mattkranzler5 commented 9 years ago

@tbroyer the problem is that Dagger 2 won't let you override the @Provides methods.

tbroyer commented 9 years ago

@mattkranzler5 Just remove the @Module annotation as well.

mattkranzler5 commented 9 years ago

@tbroyer how would you handle injecting test classes if you don't provide a test component? If your test source is outside of your main source you wouldn't be able to add inject methods to your main component nor would you want to.

tbroyer commented 9 years ago

@mattkranzler5 This is not what your example showed. But given that you're not "reshaping" the graph in your modules with that approach, you can still declare a test component referencing the prod modules for the static analysis, and give it the test modules at runtime.

What many of you seem to forget is that Dagger 2 builds the whole dependency graph at build time based on static analysis of the classes (and this is precisely what makes it extremely lightweight at runtime); so you cannot change the shape of the graph at runtime, and if you need a differently-shaped graph for your tests, then you need a different graph: different component(s) and different module(s), referencing different injectable classes.

BTW, I'm curious whether one couldn't/shouldn't use Guice during tests (some, at least) with its new DaggerAdapter.

mattkranzler5 commented 9 years ago

@tbroyer I just verified that as long as you don't annotate your module subclasses as @Module and the overridden @Provides classes everything works and you can override your dependencies for testing purposes. Thank you for pointing this out!

stephanenicolas commented 9 years ago

If that works, could a sample illustrate the technique ? Le 2015-09-10 12:19, "Matt Kranzler" notifications@github.com a écrit :

@tbroyer https://github.com/tbroyer I just verified that as long as you don't annotate your module subclasses as @Module and the overridden @Provides classes everything works and you can override your dependencies for testing purposes. Thank you for pointing this out!

— Reply to this email directly or view it on GitHub https://github.com/google/dagger/issues/110#issuecomment-139299804.

mattkranzler5 commented 9 years ago

@stephanenicolas here is an updated example:

public class OverriddenModules {

    @Component(modules = {
            AppModule.class,
            NetworkModule.class
    })
    public static interface AppComponent {
         void inject(Application application);
    }

    @Module
    public static class AppModule {

        @Provides
        AppDependency provideAppDependency() {
            return new AppDependency();
        }
    }

    @Module
    public static class NetworkModule {

        @Provides
        NetworkDependency provideNetworkDependency() {
            return new NetworkDependency();
        }
    }

    @Component(modules = {
            ApplicationModule.class,
            NetworkModule.class
    })
    public static interface TestAppComponent extends AppComponent {
         void inject(TestApplication testApplication);
    }

    public static class TestAppModule extends AppModule {

        @Override
        AppDependency provideAppDependency() {
            return Mockito.mock(AppDependency.class);
        }
    }

    public static class TestNetworkModule extends NetworkModule {

        @Override
        NetworkDependency provideNetworkDependency() {
            return Mockito.mock(NetworkDependency.class);
        }
    }
}

NOTE: I did find one issue which requires a workaround still with this approach. Let's say you have a dependency that has an injected constructor and you don't have a @Provides annotated method in your parent module. If you'd like to override that dependency in a module subclass it won't work since you can't have @Provides methods in a class not annotated with @Module. Therefore you must add a @Provides method to your parent module then you can override it in your module subclass.

tbroyer commented 9 years ago

Just create a test @Module for it that you reference from your test component. Note that in this case you need/use a differently-shaped graph.

mattkranzler5 commented 9 years ago

@tbroyer that would work in some cases. In our case we have a differently scoped subcomponent which we are overriding for testing purposes during instrumentation tests in our Android app. We need to be able to swap out that scoped component for one that mocks out certain dependencies for our tests. We wouldn't be able to do what you're suggesting since it would require a differently shaped graph.

felipecsl commented 9 years ago

@mattkranzler5 that's a great hack. Worked perfectly (after tweaking it for a bit, your code sample is not 100% correct I think). Thanks a lot!

stephanenicolas commented 9 years ago

There is also an alternative mentioned recently in Android Weekly : http://blog.sqisland.com/2015/04/dagger-2-espresso-2-mockito.html

@mattkranzler5 , the test and prod components can be declared in different trees (test vs. main), right ?

Also @mattkranzler5 , I didn't fully get the edge case. What you did is exactly providing a way to override a dependency declared in a parent module, exactly what was searched for...

netdpb commented 8 years ago

We have some documentation on strategies for doing testing of classes and applications that use Dagger. We're working on getting that documentation synced out here.

In the meantime, see the discussion starting at https://github.com/google/dagger/issues/186#issuecomment-163309550.