'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 |
