fabioCollini / DaggerMock

A JUnit rule to easily override Dagger 2 objects
Apache License 2.0
1.16k stars 91 forks source link

Access to the dependency component #12

Closed plastiv closed 6 years ago

plastiv commented 8 years ago

Currently, it is possible to create component and overwrite module factories to return mock objects using dagger mock rule. Even if @Component has declared @Component(dependencies=..).

@Rule
public final DaggerMockRule<ActivityComponent> daggerMockRule =
            new DaggerMockRule<>(ActivityComponent.class, new ActivityModule())
                    .addComponentDependency(ApplicationComponent.class, new ApplicationModule(application))
                    .set(this);

It is also possible to get constructed component instance from DaggerMockRule.ComponentSetter, but not possible to get instance of the dependent Component?

I may be doing something wrong, but like at the snippet above I have interface segregation for Components (StorageComponent, NetworkComponent, ApplicationComponent, ActivityComponent) and I want to get access to the created instances of all dependent Components.

Either by providing ComponentSetter for addComponentDependency call:

public<D> DaggerMockRule<C> addComponentDependency(
                 DaggerMockRule.ComponentSetter<D> dependencyComponentSetter
                 Class<D> componentClass, 
                 Object... modules);

Or by providing already constructed instance to the dagger mock rule, like daggerMockRule.addComponentDependencyInstance(componentInstance) where instance would be constructed manually or by another dagger mock rule.

Does it sounds like useful feature to you?

fabioCollini commented 8 years ago

Hi, I just added support to use InjectFromComponent annotation to inject objects defined in dependent components. Is this feature useful for your use case? You can try to use it using 12e97d9b51 as DaggerMock version.

plastiv commented 8 years ago

Thank you for a quick reply and nice addition!

On android we have classes which instantiation is controlled by framework and it's not possible to override constructors to provide clear dependency list. Instead, in our application we rely on an additional level of indirection, where such classes are using ComponentHolder.get().inject(this) call to bind dependency from dagger component to the class instance.

To be able to test this kind of classes I need to switch Component and this is why DaggerMockRule.ComponentSetter was added (getApplication().setComponent() from Readme). I guess for the same reason I want to be able to switch base dependent Component as well.

I do understand though that you may not want to bloat library api for every case. Lets see if other users are interested in this feature.

fabioCollini commented 8 years ago

I usually don't use dependent components in this way, in the app I am working on I am using a single component with multiple modules and some subcomponents. Could you provide an example of a multiple components configuration? Are the components annotated with a scope? It doesn't seem too complicated to implement this modification but I need an example to reproduce the use case.

plastiv commented 8 years ago

I need an example to reproduce the use case.

Let me try to explain the case where it can be useful.

Let's say we have an activity

public class MainActivity extends AppCompatActivity  {

    @Inject MainPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        App app = (App) getApplication();
        app.getComponent().activityComponent(new MainActivityModule(this)).inject(this);

        presenter.loadData();
    }
}

What if we have more complex components and instead of mocking SnackBarManager and RestService we want to mock and verify behavior of the MainPresenter itself? Same trick as application component would be required. Instead of using constructor directly:

App app = (App) getApplication();
app.getComponent().activityComponent(new MainActivityModule(this)).inject(this);

We can use

getActivityComponent().inject(this);

Where getActivityComponent() implementation is

private ActivityComponent testComponent;

public ActivityComponent getActivityComponent() {
   if (testComponent != null) {
     return testComponent;
   } 
   return getApplicationComponent().activityComponent(new MainActivityModule(this));
}

@VisibleForTesting
protected void setActivityComponent(ActivityComponent testComponent){
   this.testComponent = testComponent;
}

Where getApplicationComponent() is doing the same - returns testApplicationComponent if set. Let's imagine that activity uses both activity and application component dependencies

class MainActivity extends AppCompatActivity  {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getActivityComponent().activityService().doAction();
    }

   void somMethodInsideOfSomeOtherClassWhichThisActivityHasCalled() {
      getApplicationComponent().applicationService().doAction();
   }
}

Maybe it's just me, but because of I'm using both calls getApplicationComponent() and getActivityComponent() I need to be able to setup both instances at test, which is impossible right now due to having only one component setter, which passes either application or activity component.

It could be just me, but I found impractical for now to provide only activityComponent to access dependencies inside of the activity. I found more convenient to structure the app by having separation of interfaces where I have SerializationComponent, NetworkComponent, StorageComponent, ApplicationComponent, ActivityComponent, etc. And if class needs GSON parser to use I'm providing it with SerializationComponent, instead of ActivityComponent. Which is also useful for java tests, where I can use gson directly, without mocking whole android context world.

Back to the original feature request. It would be nice to have an api which allows when using DaggerMockRule to construct not single component instance, but several ones (like dependent or sub components) to access them. Either by having callback similar to current ComponentSetter or by allowing to pass dependent or sub component instances constructed before to the DaggerMockRule which will instead of creating new one reuse previously created ones.

P.S. I'm sorry for the wall of text. DaggerMockRule lib is great it enables turbo boost for writing both unit and android tests in our application. I appreciate the time & effort you put into developing this library. And I understand that you may want to have more time to incubate the different ideas and cases to see if they fit.

fabioCollini commented 8 years ago

Hi, I have added a set method to DaggerMockRule class with a class parameter. You can use it to define a setter on a dependent component. An example is in this test: https://github.com/fabioCollini/DaggerMock/blob/master/lib/src/test/java/it/cosenonjaviste/daggermock/dependency/SetDependentComponentTest.java You can try it using version c4ae55f6fa, could you tell me if it works for you? Thanks for your help and for the detailed explanation!

plastiv commented 8 years ago

Hi! I have checked the c4ae55f version and it works for me. Thank you! 👍

ricamgar commented 7 years ago

Hi! I'm using a dependent component as well, but I'm not managing to make it work with the addComponentDependency method.

My dependent component has a module which is initialized with the Context. It needs the context to initialize some dependencies like Picasso.

Well, the problem is that when running the test, Picasso is trying to get the context from the Module, but this context is null...

My configuration is like this:

@Component(
  modules = arrayOf(ChatModule::class),
  dependencies = arrayOf(CommonComponent::class)
)
interface ChatComponent{
  fun getRepository(): Repository
}
@Module
class ChatModule{
  @Provides
  fun provideRepository() : Repository {
    return RepositoryImpl()
  }
}
@Component(modules = arrayOf(CommonModule::class))
interface CommonComponent{
  fun getApplicationContext()
  fun getPicasso()
}
@Module
class CommonModule(private val context: Context){
  @Provides
  fun provideApplicationContext(): Context = context

  @Provides
  fun providePicasso(context: Context): Picasso {
    return Picasso.Builder(context).build()
  }
}

I'm declaring the @Rule like this:

private val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext

@Rule
val rule: DaggerMockRule<ChatComponent> = 
  DaggerMockRule<ChatComponent>(ChatComponent::class, ChatModule())
    .addComponentDependency(CommonComponent::class, CommonModule(context))
    .set {
      component -> Application.chatComponent = component
    }

As I said, when Dagger tries to initialize Picasso, it throws the exception: Cannot return null from a non-@Nullable @Provides method

Here the stacktrace:

Caused by: java.lang.NullPointerException: Cannot return null from a non-@Nullable @Provides method
at dagger.internal.Preconditions.checkNotNull(Preconditions.java:48)
at com.muba.common.CommonModule_ProvideApplicationContext$common_debugFactory.get(CommonModule_ProvideApplicationContext$common_debugFactory.java:19)
at com.muba.common.CommonModule_ProvideApplicationContext$common_debugFactory.get(CommonModule_ProvideApplicationContext$common_debugFactory.java:8)
at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)
at com.muba.common.CommonModule_ProvidePicasso$common_debugFactory.get(CommonModule_ProvidePicasso$common_debugFactory.java:33)
at com.muba.common.CommonModule_ProvidePicasso$common_debugFactory.get(CommonModule_ProvidePicasso$common_debugFactory.java:11)
at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)
at com.muba.common.DaggerCommonComponent.getPicasso(DaggerCommonComponent.java:143)
at com.muba.chat.di.DaggerChatComponent$com_muba_common_CommonComponent_getPicasso.get(DaggerMainComponent.java:468)
at com.muba.chat.di.DaggerChatComponent$com_muba_common_CommonComponent_getPicasso.get(DaggerChatComponent.java:458)
at com.muba.chat.di.DaggerChatComponent.getPicasso(DaggerChatComponent.java:303)

Thanks in advance for your support!

fabioCollini commented 7 years ago

Hi, it can be something related to Kotlin (all the classes are final in Kotlin). What are you using to open all the kotlin classes? Are you using dexopener test runner? Thanks for your report

ricamgar commented 7 years ago

Hi, do you mean the Modules? They are open, I forgot to put the keyword in my previous example... open class ChatModule and open class CommonModule. I tried using dexopener also with no success...

The full exception is:

java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at it.cosenonjaviste.daggermock.ComponentOverrider$1.answer(ComponentOverrider.java:48)
at org.mockito.internal.handler.MockHandlerImpl.handle(MockHandlerImpl.java:97)
at org.mockito.internal.handler.NullResultGuardian.handle(NullResultGuardian.java:32)
at org.mockito.internal.handler.InvocationNotifierHandler.handle(InvocationNotifierHandler.java:36)
at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.doIntercept(MockMethodInterceptor.java:57)
at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.doIntercept(MockMethodInterceptor.java:43)
at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor$DispatcherDefaultingToRealMethod.interceptAbstract(MockMethodInterceptor.java:137)
at com.muba.chat.di.ChatComponent$MockitoMock$1030565205.getPicasso(Unknown Source)
at com.flinkers.flinky.presentation.chat.conversation.ConversationsListActivityTest.showConversationsIfNotEmpty(ConversationsListActivityTest.kt:62)
at java.lang.reflect.Method.invoke(Native Method)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at it.cosenonjaviste.daggermock.DaggerMockRule$1.evaluate(DaggerMockRule.java:112)
at android.support.test.internal.statement.UiThreadStatement.evaluate(UiThreadStatement.java:55)
at android.support.test.rule.ActivityTestRule$ActivityStatement.evaluate(ActivityTestRule.java:270)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:59)
at android.support.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:262)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1932)
Caused by: java.lang.NullPointerException: Cannot return null from a non-@Nullable @Provides method
at dagger.internal.Preconditions.checkNotNull(Preconditions.java:48)
at com.muba.common.CommonModule_ProvideApplicationContext$common_debugFactory.get(CommonModule_ProvideApplicationContext$common_debugFactory.java:19)
at com.muba.common.CommonModule_ProvideApplicationContext$common_debugFactory.get(CommonModule_ProvideApplicationContext$common_debugFactory.java:8)
at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)
at com.muba.common.CommonModule_ProvidePicasso$common_debugFactory.get(CommonModule_ProvidePicasso$common_debugFactory.java:33)
at com.muba.common.CommonModule_ProvidePicasso$common_debugFactory.get(CommonModule_ProvidePicasso$common_debugFactory.java:11)
at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)
at com.muba.common.DaggerCommonComponent.getPicasso(DaggerCommonComponent.java:143)
at com.muba.chat.di.DaggerChatComponent$com_muba_common_CommonComponent_getPicasso.get(DaggerChatComponent.java:468)
at com.muba.chat.di.DaggerChatComponent$com_muba_common_CommonComponent_getPicasso.get(DaggerChatComponent.java:458)
at com.muba.chat.di.DaggerChatComponent.getPicasso(DaggerChatComponent.java:303)
fabioCollini commented 6 years ago

In Kotlin the methods are closed too. Are you defining the module methods as open?

ricamgar commented 6 years ago

Hi! It worked opening all the methods. But... on the Rule when I get the component, it is a Mock, but the call to component.getRepository() returns the real repository, not the Mock... Pretty sure I'm doing something wrong.. but I can't figure out what exactly. Thanks for your help!

fabioCollini commented 6 years ago

Can you paste the code of the test? Are you defining the repository in a field? How do you retrieve the component?

ricamgar commented 6 years ago

Here the code:

private val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext

@Rule
@JvmField
val rule: DaggerMockRule<ChatComponent> =
  DaggerMockRule<ChatComponent>(ChatComponent::class.java, ChatModule(context))
    .addComponentDependency(CommonComponent::class.java, CommonModule(context))
    .set { component ->
      ChatApplication.chatComponent = component
    }

@Mock
lateinit var repository: Repository

@Rule
val activityRule = ActivityTestRule(ListActivity::class.java, false, false)

Then on the test method, I try to mock the response of one repository method with:

`when`(repository.conversations()).thenReturn(Flowable.just(conversations))

but it doesn't work because the repository is not a mock.

fabioCollini commented 6 years ago

It should work because the repository is defined in the module connected to the main component (it's the standard DaggerMock example). And the repository field is defined in the test using Mock annotation, how can it be populated with the real object? Maybe the repository is null? When do you define the behaviour of the mock using when thenReturn?

ricamgar commented 6 years ago

Yes, when it comes to the line into the @Test where I want to use when().return() the repository is null. But I don't know why...

fabioCollini commented 6 years ago

DaggerMockRule uses MockitoAnnotations.initMocks to init fields annotated with @Mock so it should work. However you can try to use Mockito Kotlin to simplify your code, instead of using @Mock you can write:

val repository = mock<Repository>
ricamgar commented 6 years ago

Hi, is it normal behavior, inside the Rule, when I call component.getRepository() is returning the real implementation? I mean, the component is a mock, but the call to getRepository returns the real implementation.

Thanks for your support!

ricamgar commented 6 years ago

Problem found! :) It is not mocking the repository because I'm providing it with the @Binds annotation. At the moment I provide it creating the instance in the module, it works perfectly!