'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() {

    }
}


Solution 1:[1]

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.

Solution 2:[2]

Based on Nikola's advice and referred to Google Codelab, you can use WorkManagerTestRule to create your own WorkParameters as following

@get:Rule
var wmRule = WorkManagerTestRule()

...

val configuration = wmRule.configuration
val executor = configuration.executor
val workManagerTaskExecutor = WorkManagerTaskExecutor(executor)
val workDatabase = WorkDatabase.create(
    context,
    workManagerTaskExecutor.backgroundExecutor,
    true
)

// you can customize your UUID, input data based on your scenario
val workParameters = WorkerParameters(
    UUID.fromString("d1e5a17a-bed4-11ec-9d64-0242ac120002"),
    Data.EMPTY,
    listOf(),
    WorkerParameters.RuntimeExtras(),
    1,
    executor,
    workManagerTaskExecutor,
    configuration.workerFactory,
    WorkProgressUpdater(workDatabase, workManagerTaskExecutor),
    WorkForegroundUpdater(
        workDatabase,
        object : ForegroundProcessor {
            override fun startForeground(
                 workSpecId: String,
                 foregroundInfo: ForegroundInfo
            ) {

            }

            override fun stopForeground(workSpecId: String) {

            }
        },
        workManagerTaskExecutor
   ))
          

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2