Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can you UnitTest Android workers that employ Hilt constructor injection

Im investigating the use of Hilt in my current Android application.

api 'androidx.hilt:hilt-work:1.0.0-alpha02'
implementation "com.google.dagger:hilt-android:2.30.1-alpha"
kapt 'com.google.dagger:hilt-android-compiler:2.30.1-alpha'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'

api "androidx.work:work-runtime:2.4.0"
implementation "androidx.work:work-runtime-ktx:2.4.0"

testImplementation "androidx.work:work-testing:2.4.0"

testImplementation 'com.google.dagger:hilt-android-testing:2.30.1-alpha'
kaptTest 'com.google.dagger:hilt-android-compiler:2.30.1-alpha'

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.7"
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "junit:junit:4.13.1"
testImplementation "org.robolectric:robolectric:4.4"
testImplementation 'io.mockk:mockk:1.10.3'
testImplementation "androidx.test:core:1.3.0"
testImplementation "androidx.test.ext:junit:1.1.2"

I cannot get my androidx.work.CoroutineWorker UnitTest to run though.

They fail with this exception:-

java.lang.IllegalStateException: Could not create an instance of ListenableWorker com.my.manager.background.work.worker.MyWorker

    at androidx.work.testing.TestListenableWorkerBuilder.build(TestListenableWorkerBuilder.java:361)
    at com.my.manager.background.work.ApplicationFeaturesWorkerTest.testApplicationFeaturesWorker(MyWorkerTest.kt:13)
    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 org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
    at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:61)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:575)
    at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:263)
    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)

My Worker constructor resembles this:-

class MyWorker @WorkerInject constructor(@Assisted context: Context, @Assisted params: WorkerParameters, private val service: MyApi) : BaseWorker(context, params) {
}

The Hilt documentation states:-

End-to-end tests

For integration tests, Hilt injects dependencies as it would in your production code. Testing with Hilt requires no maintenance because Hilt automatically generates a new set of components for each test.

What am I doing wrong?

What do I need to change to enable my pure UnitTests to be able to create instances of Android CoroutineWorker that employ Hilt constructor injection?

UPDATE

My UnitTest resembles this:-

class MyWorkerTest : BaseTest() {

    @Test
    fun testMyWorker() {
        val worker = TestListenableWorkerBuilder<MyWorker>(mContextMock).build()
        runBlocking {
            val result = worker.doWork()

            assert(result == ListenableWorker.Result.success())

        }
    }
}

My BaseTest class:-

@RunWith(AndroidJUnit4::class)
@Config(manifest = Config.NONE, sdk = [O, O_MR1, P, Q])
abstract class BaseTest {

    lateinit var executor: Executor

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    val mContextMock: Application = ApplicationProvider.getApplicationContext()

    @Before
    fun setup() {

        executor = Executors.newSingleThreadExecutor()
    }

    fun manufactureClient(): OkHttpClient {

        return OkHttpClient.Builder()
            .connectTimeout(OK_HTTP_CLIENT_TIMEOUT, TimeUnit.SECONDS)
            .readTimeout(OK_HTTP_CLIENT_TIMEOUT, TimeUnit.SECONDS)
            .writeTimeout(OK_HTTP_CLIENT_TIMEOUT, TimeUnit.SECONDS)
            .callTimeout(OK_HTTP_CLIENT_TIMEOUT, TimeUnit.SECONDS)
            .followSslRedirects(true)
            .retryOnConnectionFailure(true)
            .followRedirects(true)
            .addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BASIC
            }).build()
    }

    @After
    fun tearDown() {

    }
}
like image 958
Hector Avatar asked Oct 14 '22 23:10

Hector


1 Answers

If you need to unit test it, then you might not need to use TestListenableWorkerBuilder to instantiate Worker, but instantiate them as any other class.

class MyWorkerTest : BaseTest() {
    private lateinit var worker : MyWorker 
     
    @Before
    fun setupWorker(){ 
        worker = MyWorker(mockContext, mockOtherClass)
    } 
    @Test
    fun testMyWorker() = runBlocking {
       val result = worker.doWork()
       verify { mockOtherClass.someFunction() }
       //other assertions
      Unit 
    }
}

Unit testing Worker has to test correct calls and behavior inside the Worker's doWork().

TestListenableWorkerBuilder is used in instrumentation tests. Also hilt and dagger are suggested to be avoided for unit tests.

like image 95
Nikola Despotoski Avatar answered Oct 27 '22 10:10

Nikola Despotoski