Testing WorkManager Worker classes which use Assisted Dependency Injection
Table of contents
This article is for someone who wants to write tests for the business logic inside their Worker classes that use Assisted Dependency Injection.
Suppose you have a Worker which looks like below and you want to test its business logic.
@HiltWorker
class DemoWorker @AssistedInject constructor(
// provided by WorkManager
@Assisted ctx: Context,
// provided by WorkManager
@Assisted params: WorkerParameters,
// not provided by WorkManager
private val foo: Foo()
) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
foo.someFunction()
}
}
Here Dagger-Hilt has been used for DI but that won't matter as for testing we won't be using any DI framework.
Testing Business logic is not the aim here but how to set up our Tests so that a Worker class that uses assisted Injection can be tested is.
I have attached the link to the GitHub repository at the end of the blog with the complete code but I would recommend you follow along.
To start create a new Android Project and add the following dependencies inside app-level build.gradle
file
// work manager
implementation "androidx.work:work-runtime-ktx:2.7.1"
implementation("androidx.hilt:hilt-work:1.0.0")
// Testing WorkManager
androidTestImplementation "androidx.work:work-testing:2.7.1"
// For Making Assertions in Test Cases
androidTestImplementation "com.google.truth:truth:1.1.3"
Let's create and write some minimal business logic inside a Worker class for the sake of better understanding. Create a class SyncWorker
inside the main source set
This is what our SyncWorker looks like.
package com.raghav.workmanagertesting
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.raghav.workmanagertesting.repository.IRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted ctx: Context,
@Assisted params: WorkerParameters,
private val repository: IRepository
) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
return if (repository.refreshLocalDatabase()) {
Result.success()
} else {
Result.failure()
}
}
companion object {
private const val TAG = "SyncWorker"
}
}
IRepository
is an Interface that will later help us to use a fake version of
the real repository class which will be named RealRepository
and will contain the actual code used in production located at com.raghav.workmanagertesting.repository
as the purpose is to test SyncWorker
and not the RealRepository
itself. Also, it is advised to use fakes or mocks of dependencies of the class under test to create a controlled testing environment.
package com.raghav.workmanagertesting.repository
interface IRepository {
suspend fun refreshLocalDatabase(): Boolean
}
Our RealRepository
class will take two parameters api
(Remote DataSource) and dao
(Local DataSource) and will contain a function refreshLocalDatabase()
which performs two operations:
1. Fetches data from a remote api
2. Saves this data inside our database
If any exception occurs while performing any of the above operations it will return False
otherwise True
.
We need to do this operation periodically with an interval of 1 hour so we will use SyncWorker
for this operation.
This is how our RealRepository
class will look
package com.raghav.workmanagertesting.repository
import com.raghav.workmanagertesting.SampleApi
import com.raghav.workmanagertesting.SampleDatabaseDao
import javax.inject.Inject
class RealRepository @Inject constructor(
private val api: SampleApi,
private val dao: SampleDatabaseDao
) : IRepository {
override suspend fun refreshLocalDatabase(): Boolean {
return try {
val items = api.getItemsFromApi()
dao.saveItemsInDb(items)
true
} catch (e: Exception) {
false
}
}
}
Our Worker returns Result.success()
if the local database was successfully refreshed and Result.failure()
if there was an exception.
Inside androidTest
source set of your project create a class SyncWorkerTest
This is where we will write test cases to test business logic o SyncWorker
What are we going to Test?
We will write a test to confirm that if any error occurs while fetching data from the remote api our SyncWorker will return Result.failure()
Inside SyncWorkerTest
, create the following function with @Test
annotation.
package com.raghav.workmanagertesting
import org.junit.Test
@Test
fun ifErrorInFetchingResponseThenReturnFailure(){
}
In Production code we have to first get an Instance of WorkManager then create a work request and then enqueue our work request something like thisval myWorkRequest = OneTimeWorkRequest.from(MyWork::
class.java
) WorkManager.getInstance(myContext).enqueue(myWorkRequest)
But since Workmanager 2.1.0 classes TestWorkerBuilder
and TestListenableWorkerBuilder
were provided to test business logic inside Worker and Coroutine Worker classes respectively without needing to instantiate WorkManager in the test cases.
Also as we are inside the androidTest
source set we can get context with the help ofApplicationProvider.getApplicationContext<Context>()
and our test case will now look like this
package com.raghav.workmanagertesting
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.testing.TestListenableWorkerBuilder
import org.junit.Test
@Test
fun ifErrorInFetchingResponseThenReturnFailure() {
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<SyncWorker>(
context = context
).build()
}
What's left is to call our SyncWorker's
doWork()
method and assert the result, right? Emphasis on right here.
package com.raghav.workmanagertesting
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Test
@Test
fun ifErrorInFetchingResponseThenReturnFailure() {
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<SyncWorker>(
context = context
).build()
runBlocking {
val result = worker.doWork()
assertThat(result).isEqualTo(Result.failure())
}
}
As
doWork()
is a suspend function we can only call from a Coroutine so I have used runBlocking Coroutine builder which is often used for testing purposes but hardly in a production environment.
If we will run this test we will get an Error!
java.lang.IllegalStateException: Could not create an instance of ListenableWorker
The Error says that an Instance of Listeneable Worker which in our case is SyncWorker
cannot be created. Therefore we have to provide a factory that will create SyncWorker
in the test environment because in the production environment, Hilt automatically takes care of this by HiltWorkerFactory.
Let's create a file called TestWorkerFactory inside androidTest
Source set.
In this class, we will write the logic for the creation of our SyncWorker
in a test environment.
package com.raghav.workmanagertesting
import android.content.Context
import androidx.work.ListenableWorker
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
class TestWorkerFactory : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker {
return SyncWorker(
appContext,
workerParameters,
// instance of repository
)
}
}
We will pass a Fake Implementation of RealRepository
for testing purposes.
Create a class FakeRepository
inside androidTest
source set and copy the following content.
package com.raghav.workmanagertesting
import com.raghav.workmanagertesting.repository.IRepository
import com.raghav.workmanagertesting.util.FakeApi
import com.raghav.workmanagertesting.util.FakeDao
class FakeRepository(
private val fakeApi: FakeApi = FakeApi(),
private val fakeDao: FakeDao = FakeDao()
) : IRepository {
override suspend fun refreshLocalDatabase(): Boolean {
return try {
val items = fakeApi.getItemsFromApi()
if (items.isNullOrEmpty()) {
throw Exception("Error occurred while fetching response")
}
fakeDao.saveItemsInDatabase(items)
true
} catch (e: Exception) {
false
}
}
}
FakeApi
and FakeDao
, as the name suggests, are fake versions of real api
and dao
used inside RealRepository
to emulate real-world conditions without actually using retrofit or Room. Let's create a package called util
an add both with the following content.
package com.raghav.workmanagertesting.util
import com.raghav.workmanagertesting.SampleResponseItem
class FakeApi {
// to mock successful response
// fun getItemsFromApi() = listOf(
// SampleResponseItem(1, "title1"),
// SampleResponseItem(1, "title2")
// )
// to mock failure response
fun getItemsFromApi(): List<SampleResponseItem>? = null
}
package com.raghav.workmanagertesting.util
import com.raghav.workmanagertesting.SampleResponseItem
class FakeDao {
fun saveItemsInDatabase(items: List<SampleResponseItem>) {
}
}
Now as we have our FakeRepository
ready we can just provide it in our TestWorkerFactory
while creating SyncWorker
.
package com.raghav.workmanagertesting
import android.content.Context
import androidx.work.ListenableWorker
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
class TestWorkerFactory : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker {
return SyncWorker(
appContext,
workerParameters,
FakeRepository()
)
}
}
What's left is to provide this factory to the TestListenableWorkerBuilder
inside our SyncWorkerTest
class through the setWorkerFactory()
method and our final test class will look like this.
package com.raghav.workmanagertesting
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Test
class SyncWorkerTest {
@Test
fun ifErrorInFetchingResponseThenReturnFailure() {
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<SyncWorker>(
context = context
).setWorkerFactory(TestWorkerFactory())
.build()
runBlocking {
val result = worker.doWork()
assertThat(result).isEqualTo(Result.failure())
}
}
}
If you will now run your test it will run and execute successfully!
We have reached our aim and here is the link to the GitHub repository
https://github.com/avidraghav/TestingWorkManager
If you liked the blog then do comment or react. Any Feedback will be highly appreciated ๐
Connect with me on LinkedIn and Twitter
Here are links to some of my other blogs:
Testing Strategy and its Theory in Android
Code Compilation Process in Android
Intercepting Network Requests in Android via OkHttp Interceptor.
Subscribe to my newsletter
Read articles from Raghav Aggarwal directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Raghav Aggarwal
Raghav Aggarwal
Android Engineer at Mutual Mobile