Mocking is not rocketscience: MockK features

3nCW...dbJq
7 Apr 2023
17

MockK is definitely a better altern- ative to other mocking frameworks if you use Kotlin. Previous article showed some basics. Now time to talk about such features as captured arguments, relaxed mocks, spies and annotations.
Capturing

Argument capturing can make your life easier if you need to get a value of an argument in every or verify block. Let's say that we have following class:

class Divider (

fun divide(a: Int, b: Int) = a / b

There are two ways to capture argu- ments: using CapturingSlot Int> and using MutableList<Int>.

CapturingSlot allows to capture only one value, so it is simpler to use.

val slot slot<int>()

val mock mockk Divider>() every mock.divide (capture(slot), any())) returns 22

This creates a slot and a mock and

set expected behavior following
way: in case mock. divide is called, then first argument is captured to the slot and 22 is returned.

Now code being tested:

mock.divide(5, 2) // 22 is a result

After executing it, slot. captured value is equal to first argument i.e. 5 Now you can do some checks, assert

for example:

assertEquals(5, slot.captured)

Besides that, you can use a slot in an answer lambda:

every {

mock.divide (capture(slot), any()s

} answers ( slot.captured 11

}

So mock.divide(5, 2) returns 55.
That is basically it. Working with MutableList is the same, just in- stead of a slot in capture function MutableList should be used.

val list mutableList<Int>() val mock mockk<Divider>() every mock.divide (capture(list), any()) } returns 22

This allows capturing several values from several calls in a row.

Relaxed mocks

By default mocks are strict. Before passing mock to a code being tested you should set behavior with every block. In case you do not provide expected behavior, and call is performed, library throws an exception.
This is different from what Mockito is doing by default. Mockito allows you to skip specifying expected behavior and replies with some basic value alike null or 0. You can achieve the same and even more in Mockk by declaring relaxed mock.

val mock = mockk<Divider>(relaxed = true)

Then you can use it right away:

mock.divide(5, 2) // returns 0

Besides that, if the return value is of reference type, library would try to create child mock and build chain of calls.

mock.call(1, 2, 3).call2(4, 5, 6)

In verify blocks you can check if that calls were performed:
verify { mock.divide(5, 2)}

and

verify { mock.call(1, 2, 3).call2(4, 5,

6)}

Note however that due to natural language limits it is not possible to have chains of calls work for generic return types as this inform- ation got erased. If you need such use-case, set expected behavior in every block explicitly.

Spl

Spies give the possibility to set expected behavior and do behavior verification while still executing original methods of an object.

Let's say we have the following

class:
class Adder ( fun magnify(a: Int) = a fun add(a: Int, b: Int) = a + magnify(b)

}

We want to test the behavior of add function while mocking behavior of magnify function.

Let's first create a spy:

val spy = spyk(Adder())

Here we create object Adder () and build a spy on top of it. Building a spy actually means creating a spe- cial empty object of the same type and copying all the fields.

Now we can use it, as if it was regular Adder() object.

assertEquals(9, spy.add(4, 5))
This checks that original method is called.

Besides that, we can define spy behavior by putting it to every block:

every spy.magnify (any()) } answers { firstArg<Int>() * 2 }

This provides an expected answer for magnify function.

After that, behavior of add has changed because it was dependent on magnify:

assertEquals(14, spy.add(4, 5))

So we achieved our goal using spy. Additionally, we can verify calls, as if it was a mock:

verify spy.add(4, 5) } verify spy.magnify(5) }
This is a very powerful testing

technique to employ.

Annotations

The library supports annotations @MockK, @SpyK and @RelxedMockK, which can be used as a simpler way to create mocks, spies, and relaxed mocks correspondingly.

class Test {

@Mockk lateinit var doc1: Dependency1

@RelaxedMockk

lateinit var doc2: Dependency2

@spyk

val doc3 = Dependency3()

@Before fun setup() = MockkAnnotations.init(this)

@Test

fun calculateAddsValues() { every doct.call().add(any())) every doc2.value2 ) returns "6" every doc3.sub(any())) returns
The important part here is MockKAnnotations.init(this) call which is executed at @Before phase. When it is executed all annotated properties are substituted with corresponding objects: mocks, spies and relaxed mocks.

Next article describes chained calls, object mocks, extension functions, and DSLS.

Thank you for your attention!

Clap to say "thank you" and to help others find this article.
Next article about unit testing with MockK will be published next week. Don't miss it - subscribe to public- ation and author's channel. Here is the previous article:

Write & Read to Earn with BULB

Learn More

Enjoy this blog? Subscribe to Thar

1 Comment

B
No comments yet.
Most relevant comments are displayed, so some may have been filtered out.