According to the Android developer guides, WorkManager is an API that makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or the device restarts.
Testing WorkManager
Let's look at a common scenario, assuming I want to backup user data to my server or maybe update the app data. Following the TDD school of thought, we should write the tests first before writing any code. But first, a rough idea of what I want to do before I write the test.
Using the backing up data scenario;
- I'd like this to be done periodically
- I'd want to be sure the device has an available network connection
- The device of user should have sufficient battery or is probably charging.
- This task should be _re-doable_, i.e if the network fails, the task will run again.
Looking at these, this is what is referred to as constraints
in WorkManager (Kind of like the constraints
in the ConstraintLayout
)
Now that I have these in mind, I'll add the Gradle dependencies for testing WorkManager and sync.
// Work test dependencies
androidTestImplementation 'androidx.work:work-testing:2.3.4'
I am gonna create a class in the androidTest
directory and call it BackupWorkerTest
.
Just a side note, tests that do not need the android platform to run but just the JVM are written in the test directory and they are sometimes referred to as local tests, while tests that require the android platform are written in the androidTest directory, these tests can also be referred to as Instrumented tests.
Setup test
Okay, I am gonna add a few lines that will run before our actual test does
private lateinit var context: Context
@Before
fun setup() {
// init context
context = ApplicationProvider.getApplicationContext()
}
Next, I'll create a function to test our worker.
@Test
fun testBackupWorker() {
val worker = TestListenableWorkerBuilder<BackupWorker>(context).build()
}
BackupWorker class
Now, if we were to run it, it would fail. In fact you should see that red line on IDE. This is because I haven't created my BackupWorker
class yet. I am just gonna do that now.
class BackupWorker(appContext: Context, params: WorkerParameters) :
CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
// Mock network request
delay(2000)
return when(inputData.getInt("ResponseCode", 0)) {
200 -> Result.success()
else -> Result.retry()
}
}
}
As you've noticed, I am not actually making any request in the doWork()
function, instead I called the delay()
function that would stall the work for two seconds to mimic a network request.
I am gonna add a few more things to the test function to make it testable, what we have right now is just a call to instantiate the BackupWorker class.
Testing the BackupWorkerClass
The test function will finally look like this
@Test
fun testBackupWorker() {
val inputData = workDataOf("ResponseCode" to 200)
val worker = TestListenableWorkerBuilder<BackupWorker>(context, inputData).build()
runBlocking {
val result = worker.doWork()
assertThat(result, `is`(ListenableWorker.Result.success()))
}
}
Network Failure
As you would expect when there is network failure that the request be made again since it was unsuccessful. This will inadvertently mean that the work failed. One of the Result
I can return is Result.retry()
.
This will tell WorkManager to redo the job. So I will simulate a failed network request response code and see if the test passes.
@Test
fun testBackupWorkerNetworkError() {
val inputData = workDataOf("ResponseCode" to 404)
val worker = TestListenableWorkerBuilder<BackupWorker>(context, inputData).build()
runBlocking {
val result = worker.doWork()
assertThat(result, `is`(ListenableWorker.Result.success()))
}
}
Testing Constraints
Now I have written the tests for testing a Work. Remember I mentioned contraints at the beginning of this article and I will also need to test the Work with these contraints, let me show how I would do that.
Testing Constraints would require a different approach from what I did earlier, I am gonna put these tests in a different test class.
Setup test with constraints
As before, I will need to tell the test class to do a few things before running the actual tests, in case it would look different from what I did earlier.
private lateinit var context: Context
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
// Initialize WorkManager for testing.
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
So let me run through this a bit, as before we create a lateinit
variable that will needed later in the test, this is pretty similar to what I did before.
What is new here is initialising the WorkManager, WorkManager normally gets initialised but it is done under the hood. Adding an executor will tell the WorkManager to run in a specific thread.
Test BackupWorker With Constraints
For the BackupWorker, there are two constraints we would need to ensure on the user device before running it; a connected network and sufficient battery. Writing the tests for this constraints, we would have something like this below.
However, to simulate these constraints, I used the setAllConstraintsMet()
function of the TestDriver
.
@Test
fun testBackupWorkerWithConstraints() {
val inputData = workDataOf("ResponseCode" to 200)
// Create Constraints
val constraints = Constraints.Builder()
// Add network constraint.
.setRequiredNetworkType(NetworkType.CONNECTED)
// Add battery constraint.
.setRequiresBatteryNotLow(true)
.build()
// Create Work request.
val request = OneTimeWorkRequestBuilder<BackupWorker>()
.setInputData(inputData)
// Add constraints
.setConstraints(constraints)
.build()
val workManager = WorkManager.getInstance(context)
workManager.enqueue(request).result.get()
// Simulate constraints
WorkManagerTestInitHelper.getTestDriver(context)?.setAllConstraintsMet(request.id)
val workInfo = workManager.getWorkInfoById(request.id).get()
assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
}
Test BackupWorker Periodically
Like how most backup scenario plays out, it is done periodically. This could be user-defined or static. Remember we can run Work periodically in WorkManager, so it would be nice to test it to.
As with testing the constraints, I will need to simulate the duration between each Work. The TestDriver
class also has a function, setPeriodDelayMet()
to handle this. What this basically does is to tell the WorkManager testing framework that the duration has been completed.
@Test
fun testPeriodicBackupWorker() {
val inputData = workDataOf("ResponseCode" to 200)
// Create Work request.
val request = PeriodicWorkRequestBuilder<BackupWorker>(1, TimeUnit.DAYS)
.setInputData(inputData)
.build()
val workManager = WorkManager.getInstance(context)
// Enqueues request.
workManager.enqueue(request).result.get()
// Complete period delay
WorkManagerTestInitHelper.getTestDriver(context)?.setPeriodDelayMet(request.id)
// Get WorkInfo and outputData
val workInfo = workManager.getWorkInfoById(request.id).get()
// Assert
assertThat(workInfo.state, `is`(WorkInfo.State.ENQUEUED))
}
Yay! We've come to the end of this article. Feel free to leave suggestions and feedback below.
Link to source code below.
Top comments (0)