69

Testing Coroutines is Easy with Mockk - Josh Greenwood | Blog

 4 years ago
source link: https://blog.joshua-greenwood.com/testing-coroutines-is-easy-with-mockk/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Using coroutines to write asynchronous, non-blocking code is becoming the new norm with kotlin. However, testing asynchronous code can be difficult, especially once we need to start mocking dependencies. In this article, we’ll go over some methods of mocking both suspend and deferred functions using the fantastic framework Mockk.

What are Coroutines?

Coroutines are a new way of writing asynchronous, non-blocking code (and much more)

Simply put, a coroutine is a lightweight thread. This means that they can run in parallel and that they're also very cheap in terms of performance.

What is Mockk?

Mockk is a fantastic mocking framework for Kotlin. If you're not familiar with Mockk you may be aware of others such as Mockito or EasyMock. Classes under test may have other dependencies, using a mocking framework makes it easy to simulate the behavior of these dependencies in order to isolate the behavior of the given class under test.

In this article, for the sake of simplicity, we won't be dealing with dependencies or many classes. In the real world, you will.

Setting up

Kotlin only provides very minimal APIs in its standard library. In order follow this article you'll need to add the following dependency to your project.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}"

Of course, you'll also need Mockk.
testImplementation "io.mockk:mockk:${mockkVersion}"

We'll also be mocking out an interface, in this case, Robot. Since we’re mocking it out, we don’t really care about the implementation. For all we know, another developer is hard at work on it. Our robot can move in a given direction, but since it may take some time for it to arrive at the destination, therefore it’s a suspend function. Robot is also capable of counting nearby humans (don't ask why), in this case it returns a Deferred result, we'll cover this later in the article.

import kotlinx.coroutines.Deferred

interface Robot {
   suspend fun move(direction: Direction): Status
   suspend fun findHumansAsync(): Deferred<Int>
}

We can also go ahead and setup our test class.

private lateinit var robot: Robot
 
@Before
fun setUp() {
    robot = mockk()
}

Mocking Coroutines

If you’re familiar with Mockk you’ll be familiar with keywords such as every, verify and answers (of not don't worry!). Fortunately, to test coroutines, we don’t need to remember any new keywords or api. Simply append the prefix co to make coEvery, coVerify and coAnswers. Instead of taking a regular lambda like their counterparts, these functions take a suspend lambda.

@Test
fun `test run verify`() {
    coEvery { robot.move(Direction.SOUTH) } returns Status.OK

    runBlocking { robot.move(Direction.SOUTH) }

    coVerify { robot.move(Direction.SOUTH) }
}

Let's take this line by line:

coEvery { robot.move(Direction.SOUTH) } returns Status.OK

We're basically saying every time move is called on our robot object with the Direction.SOUTH, return Status.OK, thus mocking the behavior.

runBlocking { robot.move(Direction.SOUTH) }

Now we're executing our function move. Since it's a suspend function we'd get a warning from out IDE since suspend functions can only be called from other suspend functions. All runBlocking does is block the current thread until robot.move completes, ensuring our test code is called in the order we expect.

coVerify { robot.move(Direction.SOUTH) }

Finally, we can verify that move was called with the Direction.SOUTH. If it wasn't called or was called with the wrong parameter, our test would fail.

Results from Suspend Functions

@Test
fun `test run blocking`() {
    coEvery { robot.move(Direction.NORTH) } returns Status.OK 

    val status = runBlocking { robot.move(Direction.NORTH) }

    assertEquals(Status.OK, status)
}

runBlocking can also return the value from our suspend function. Allowing us to use the value in our asserts as usual.

Multiple Coroutines

@Test
fun `test run multiple assertOrder`() {
    coEvery { robot.move(Direction.SOUTH) } returns Status.OK
    coEvery { robot.move(Direction.EAST) } returns Status.OK

    runBlocking { robot.move(Direction.SOUTH) }
    runBlocking { robot.move(Direction.EAST) }

    coVerifyOrder {
        robot.move(Direction.SOUTH)
        robot.move(Direction.EAST)
    }
}

coVerifyOrder verifies that the given functions have been called in the order specified. coVerifyAll can also be used if order isn't important.

CoAnswers

@Test
fun `test co answers`() {
    coEvery { robot.move(Direction.WEST) } coAnswers { Status.ERROR }

    val status = runBlocking { robot.move(Direction.WEST) }

    assertEquals(Status.ERROR, status)
}

coAnswers may look similar to returns however it works a little differently. Rather than passing the result we want, we pass a lambda. This allows for more complex mocking and could be helpful in a variety of situations.

Mocking Deferred Results

A Deferred is a non-blocking cancellable future. It encapsulates a job that will complete in the future. In our case, our robot will count any humans it can see. Sometimes we may need to mock a function that returns a Deferred result.

@Test
fun deferred() {
   coEvery { robot.findHumansAsync() } returns CompletableDeferred(12)

    val status = runBlocking { robot.findHumansAsync().await() }

    assertEquals(12, status)
}

In order to mock a function that returns a deferred result requires the use of a CompletableDeferred, this is just a Deferred that can be completed (surprise surprise). This allows us to pass in the desired result as a constructor parameter.

In our runBlocking block we have to add something a little extra. We use .await() in order to get the eventual result, in this case, 12.

Summing Up

Hopefully by now you should be well on your way to testing and mocking coroutines with Mockk and I strongly recommend giving it a try if you're still using Mockito or other Java based mocking tools. Definitely check out the great documentation at Mock.io. Feel free to reach out if you have any questions or feedback.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK