This tutorial is a great example of why MockK is an excellent choice for Kotlin and how easy we can work with Kotlin features, such as the previously mentioned objects.
Of course, you can find the rest of the series on my blog, too:
- Getting Started with MockK in Kotlin [1/5]
- Verification in MockK [2/5]
- MockK: Objects, Top-Level, and Extension Functions [3/5]
- MockK: Spies, Relaxed Mocks, and Partial Mocking [4/5]
- MockK with Coroutines [5/5]
Video Content
As always, if you prefer a video content, then please check out my latest YouTube video:
Kotlin Objects and MockK
Let’s start everything by explaining how to mock Kotlin objects in MockK.
And as you know, objects are pretty specific, so we are going to see two, different examples and approaches.
mockkObject
Let’s introduce a simple example with an object:
class One { fun funToTest(): UUID = SomeObject.generateId() } object SomeObject { fun generateId(): UUID = UUID.randomUUID() }
It is not rocket science, right? We are going to simply test the function that should return the UUID generated by SomeObject
function.
So, if we would like to mock that UUID to gain more control over the behavior, then we can use the mockkObject
:
class OneTest { private val one = One() @Test fun `should return mocked ID`() { val id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") mockkObject(SomeObject) every { SomeObject.generateId() } returns id val result = one.funToTest() assertEquals(id, result) } }
The above test succeeds, and we can see that we define stubbing just like with every mock.
Moreover, if we add mockkObject
inside the function, it will not affect the scope of other tests:
class OneTest { private val one = One() @Test fun `should return mocked ID`() { val id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") mockkObject(SomeObject) every { SomeObject.generateId() } returns id val result = one.funToTest() assertEquals(id, result) } @Test fun `should fail returning mocked ID`() { val id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") val result = one.funToTest() assertEquals(id, result) } }
This time, the second test fails because SomeObject
returns a random value.
Additionally, if we use the mockkObject
, but we don’t provide stubbing, MockK will not throw any exception:
@Test fun `should fail returning mocked ID`() { val id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") mockkObject(SomeObject) val result = one.funToTest() assertEquals(id, result) }
The above test runs just like mockkObject(SomeObject)
does not exist. As a result, we get a random UUID.
So, we must be cautious about that.
unmockkObject
Although I highly doubt we should ever use that, MockK allows us to “unmock” the object with unmockkObject
function.
How does it work?
Let’s analyze the below snippet:
@Test fun `should illustrate unmockkObject`() { val id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") mockkObject(SomeObject) every { SomeObject.generateId() } returns id assertEquals(id, one.funToTest()) unmockkObject(SomeObject) assertEquals(id, one.funToTest()) }
In this example, the first assertion succeeds. However, the second assertEquals
fails due to unmockkObject
that reverts our mock.
So, again, during the second call, a random UUID is generated.
Create Mock Object Instance
I know, this title sounds simply weird.
Nevertheless, let’s take a look at another class- Two
:
class Two( private val someObject: SomeObject, ) { fun funToTest(): UUID = someObject.generateId() }
This time, we inject our SomeObject
through the constructor.
And for consistency, in MockK, we can mock that just like a simple class:
class TwoTest { private val someObject = mockk<SomeObject>() private val two = Two(someObject) @Test fun `should return mocked ID`() { val id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") every { someObject.generateId() } returns id val result = two.funToTest() assertEquals(id, result) } }
But, we must remember about two, important things in this case.
Firstly, we must provide a stubbing:
@Test fun `should fail due to missing stubbing`() { val id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") val result = two.funToTest() assertEquals(id, result) }
So, just like with classes, the above test fails due to missing stubbing!
Secondly, when defining stubbing, we must use the instance:
@Test fun `should fail due to incorrect stubbing`() { val id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") every { SomeObject.generateId() } returns id val result = two.funToTest() assertEquals(id, result) }
As we can see, we cannot refer to the SomeObject
anymore.
So, as always, the choice is up to you. 🙂
MockK and Top-Level Functions
With all of that said about objects, let’s take a look at how we can deal with another Kotlin feature, top-level functions, in MockK.
As the first step, let’s take a look at the code to test:
class Three { fun funToTest(): UUID = generateId() } fun generateId(): UUID = UUID.randomUUID()
This time, instead of the object and its function, we’re dealing with top-level generateId
.
And from the MockK perspective, everything looks pretty much the same as with objects:
class ThreeTest { private val three = Three() @Test fun `should return mocked ID`() { val id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") mockkStatic(::generateId) every { generateId() } returns id val result = three.funToTest() assertEquals(id, result) } }
As we can see, we use the mockkStatic
and we simply provide our stubbing.
And just like in the first approach in objects, nothing happens if we define the mockkStatic
, but we don’t set the stubbing. The random UUID is generated:
@Test fun `should fail due to missing stubbing`() { val id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") mockkStatic(::generateId) val result = three.funToTest() assertEquals(id, result) }
Lastly, I just wanted to mention that we have a similar function- clearStaticMockk
– on the table, too😉
Extension Functions Support
As the last thing in this article, let’s take a look at the example leveraging the extension function:
class Four { fun funToTest(): String = "abc".withCustomCase() } fun String.withCustomCase(): String = this.uppercase()
It does not make too much sense, but we can clearly see that our extension function should return an uppercase String value.
And to mock this case in MockK, we can use mockkStatic
once again:
class FourTest { private val four = Four() @Test fun `should return codersee`() { mockkStatic(String::withCustomCase) every { "abc".withCustomCase() } returns "codersee" every { "xyz".withCustomCase() } returns "not-codersee" val result = four.funToTest() assertEquals("codersee", result) } }
The really interesting part in the above snippet is that the String value must match!
Additionally, MockK allows us to mock extension functions defined in a class.
Nevertheless, in my opinion this case is so rare, that I will skip it. And if you really need to check it, then take a look at the documentation here.
Summary
That’s all for the third article, in which we learned how to deal with Kotlin objects, top-level and extension functions in MockK.
Great job, and without any further ado, let’s head to the next lesson:
- Getting Started with MockK in Kotlin [1/5]
- Verification in MockK [2/5]
- MockK: Objects, Top-Level, and Extension Functions [3/5]
- MockK: Spies, Relaxed Mocks, and Partial Mocking [4/5]
- MockK with Coroutines [5/5]