DEV Community

Ayevbeosa Iyamu
Ayevbeosa Iyamu

Posted on • Edited on

Writing WorkManager Tests

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'
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

Next, I'll create a function to test our worker.

@Test
fun testBackupWorker() {
    val worker = TestListenableWorkerBuilder<BackupWorker>(context).build()
}
Enter fullscreen mode Exit fullscreen mode

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()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()))
    }
}
Enter fullscreen mode Exit fullscreen mode

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()))
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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)