Closed plastiv closed 6 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.
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.
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.
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.
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!
Hi! I have checked the c4ae55f
version and it works for me. Thank you! 👍
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!
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
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)
In Kotlin the methods are closed too. Are you defining the module methods as open?
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!
Can you paste the code of the test? Are you defining the repository in a field? How do you retrieve the component?
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.
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
?
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...
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>
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!
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!
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=..)
.It is also possible to get constructed component instance from
DaggerMockRule.ComponentSetter
, but not possible to get instance of the dependentComponent
?I may be doing something wrong, but like at the snippet above I have interface segregation for
Component
s (StorageComponent
,NetworkComponent
,ApplicationComponent
,ActivityComponent
) and I want to get access to the created instances of all dependentComponent
s.Either by providing
ComponentSetter
foraddComponentDependency
call: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?