google / dagger

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

[Hilt] Accessing ServiceComponent bindings #2558

Closed ampeixoto closed 3 years ago

ampeixoto commented 3 years ago

I am trying to access ServiceComponent bindings in a similar way that is described in the section "Accessing ActivityComponent bindings" from the documentation. Of course in the example, it is for activities, but I suppose I should be able to use a similar strategy for ServiceComponent.

So I am using ServiceTestRule.

But the problem is that I am not event able to start/bind to the service, I get the following error:

Main looper has queued unexecuted runnables. This might be the cause of the test failure. You might need a shadowOf(getMainLooper()).idle() call.
java.lang.Exception: Main looper has queued unexecuted runnables. This might be the cause of the test failure. You might need a shadowOf(getMainLooper()).idle() call.
    at org.robolectric.android.internal.AndroidTestEnvironment.checkStateAfterTestFailure(AndroidTestEnvironment.java:502)
    at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:581)
    at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:278)
    at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.util.concurrent.TimeoutException: Waited for 5 SECONDS, but service was never connected
    at androidx.test.rule.ServiceTestRule.waitOnLatch(ServiceTestRule.java:272)
    at androidx.test.rule.ServiceTestRule.bindServiceAndWait(ServiceTestRule.java:205)
    at androidx.test.rule.ServiceTestRule.bindService(ServiceTestRule.java:149)
    at com.aizo.dataowners.heatingautomation.DummyTestServiceTest.testWithBoundService(DummyTestServiceTest.kt:42)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at androidx.test.rule.ServiceTestRule$ServiceStatement.evaluate(ServiceTestRule.java:337)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:575)
    ... 6 more

This is my DummyService:

@AndroidEntryPoint
internal class DummyTestService : Service() {

    override fun onBind(intent: Intent?): IBinder {
        return LocalBinder()
    }

    inner class LocalBinder : Binder() {
        // Return this instance of DummyTestService so clients can call public methods
        fun getService(): DummyTestService = this@DummyTestService
    }
}

And this is the test skeleton:

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@UninstallModules(
    DbModule::class,
)
@MediumTest
internal class DummyTestServiceTest {

    @get:Rule
    val serviceRule = ServiceTestRule()

    @Test
    fun testWithStartedService() {
        serviceRule.startService(
            Intent(
                ApplicationProvider.getApplicationContext<Context>(),
                DummyTestService::class.java
            )
        )

        // Add your test code here.
    }

    @Test
    fun testWithBoundService() {
        val binder = serviceRule.bindService(
            Intent(
                ApplicationProvider.getApplicationContext<Context>(),
                DummyTestService::class.java
            )
        )
        val service = (binder as DummyTestService.LocalBinder).getService()
        Assert.assertNotNull(service)
    }

}

Is there some incompatibility between ServiceTestRule/Robolectric/Hilt? Let me know if you need some more info.

bcorso commented 3 years ago

It looks like your test is missing the HiltAndroidRule. Can you try adding HiltAndroidRule as the outer rule in your test?

Also, I haven't looked into ServiceTestRule specifically, but I'm guessing it may have the same issues as ActivityScenarioRule. In particular, ActivityScenarioRule calls onCreate and performs injection before you have a chance to configure bindings in @Before, which can lead to issues, for example if the activity tries to use an injected @BindValue mock before you've had a chance to configure it (see the warning at the end of this section).

ampeixoto commented 3 years ago

@bcorso thanks for your response.

Even with the HiltAndroidRule I get the same error:

Updated test

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@MediumTest
internal class DummyTestServiceTest {

    var hiltRule = HiltAndroidRule(this)

    val serviceRule = ServiceTestRule()

    @get:Rule
    var rule = RuleChain.outerRule(hiltRule).around(serviceRule)

    @Test
    fun testWithStartedService() {
        serviceRule.startService(
            Intent(
                ApplicationProvider.getApplicationContext<Context>(),
                DummyTestService::class.java
            )
        )

        // Add your test code here.
    }

    @Test
    fun testWithBoundService() {
        val binder = serviceRule.bindService(
            Intent(
                ApplicationProvider.getApplicationContext<Context>(),
                DummyTestService::class.java
            )
        )
        val service = (binder as DummyTestService.LocalBinder).getService()
        Assert.assertNotNull(service)
    }

}

I even removed the usage of @UninstallModules in favor of @TestInstallIn but no luck. Any other suggestion?

bcorso commented 3 years ago

Hmm, the error message doesn't really look specific to something in Hilt. Could you check that things work as expected with a non-Hilt service?

ampeixoto commented 3 years ago

It seems it is not related with hilt... I removed any dependency to hilt for this test, (including the application=dagger.hilt.android.testing.HiltTestApplication config from the robolectric.properties file) and the error persists.

New version of the test:

@RunWith(AndroidJUnit4::class)
@MediumTest
internal class DummyTestServiceTest {

    @get:Rule
    val serviceRule = ServiceTestRule()

    @Test
    fun testWithStartedService() {
        serviceRule.startService(
            Intent(
                ApplicationProvider.getApplicationContext<Context>(),
                DummyTestService::class.java
            )
        )

        // Add your test code here.
    }

    @Test
    fun testWithBoundService() {
        val app = ApplicationProvider.getApplicationContext<Context>()
        val binder = serviceRule.bindService(
            Intent(
                app,
                DummyTestService::class.java
            )
        )
        val service = (binder as DummyTestService.LocalBinder).getService()
        Assert.assertNotNull(service)
    }

}

And when debugging the second test, I see that the app is instance of Application: image

Do you have any other suggestion I can try or should I open the ticket on the corresponding repo?

bcorso commented 3 years ago

@ampeixoto, unfortunately I'm not familiar with the ServiceTestRule API, so I don't have any other suggestions. I'll close this ticket. Filing a ticket with the Androidx team or asking on stack overflow may be your best bet.