Before we head there, we will take a quick look at what exactly mocking is, and the differences between several terms that often lead to confusion. Thanks to that, we will have a solid foundation before diving into the MockK and Kotlin topics.
- 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]
If you enjoy this content, then check out my Ktor Server PRO course- the most comprehensive Ktor guide in the market. You’re gonna love it! 🙂
Video Content
As always, if you prefer a video content, then please check out my latest YouTube video:
Why Do We Need Mocking?
Long story short, we need mocking to isolate the unit / the system under test.
Sounds confusing? No worries.
Let’s take a look at the typical example of a function:
So, our function A- the one we would like to focus on in our test- calls both B and C. It can happen sequentially or simultaneously; it doesn’t matter.
What matters is that when we want to test A, we want to see its behavior in different cases. How does it behave when B returns (for instance) user data successfully? What happens in case of a null value? Does it handle exceptions gracefully?
And to avoid spending countless hours on manual setup, we use the mocking technique to simulate different scenarios. Mostly with the help of libraries, like MockK.
The important thing to remember is that mocking is not limited to functions. A, B, and C may as well represent services.
And in various sources, A will be referred to as the System Under Test (SUT), whereas B, and C will be Depended On Component (DOC).
Mocking, Stubbing, Test Doubles
Frankly, please skip this section if this is your first time with mocking or MockK. I truly believe you will benefit more from focusing on learning MockK-related concepts than slight differences in wording.
Anyway, from the chronicler’s duty, let me illustrate a few concepts:
- stub is a fake object that returns hard-coded answers. Typically, we use it to return some value, but we don’t care about additional info, like how many times it was invoked, etc.
- mock is pretty similar, but this time, we can verify interactions, too. For example, to see if this was invoked with a particular ID value, exactly N times.
- stubbing means setting up a stub or a mock so that a particular method returns some value or throws an exception
- mocking means creating/using a mock
- and lastly, we use the test doubles term for any kind of replacement we use in place of a real object in your tests (that terms comes stund doubles in movies).
And if you are looking for a really deep dive, I invite you to Martin Flower’s article.
MockK Imports
With all of that said, let’s head to the practice part.
Firstly, let’s define the necessary imports for MockK.
We will be working with a plain Kotlin / Gradle project with JUnit 5, so our build.gradle.kts
dependencies should look as follows:
dependencies { testImplementation("io.mockk:mockk:1.13.17") testImplementation(kotlin("test")) } tasks.test { useJUnitPlatform() }
The io.mockk:mockk
is the only thing we need when working with MockK (unless we want to work with couroutines- but I will get back to that in the fifth lesson).
Tested Code
Then, let’s take a look at the code we are supposed to test:
data class User(val id: UUID, val email: String, val passwordHash: String) class UserRepository { fun saveUser(email: String, passwordHash: String): UUID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") fun findUserByEmail(email: String): User? = User( id = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), email = "found@codersee.com", passwordHash = "foundPwd" ) } class EmailService { fun sendEmail(to: String, subject: String, body: String) { println("Sending body $body to $to with subject $subject") } } class UserService( private val userRepository: UserRepository, private val emailService: EmailService, ) { fun createUser(email: String, password: String): UUID { userRepository.findUserByEmail(email) ?.let { throw IllegalArgumentException("User with email $email already exists") } return userRepository.saveUser(email, password) .also { userId -> emailService.sendEmail( to = email, subject = "Welcome to Codersee!", body = "Welcome user: $userId." ) } } }
As we can see, we have a simple example of a UserService
with one function- createUser
. The service and the function that we will focus on in our tests.
And although UserRepository
and EmailService
look pretty weird, the createUser
is more or less something we can find in our real-life code. We check if the given email
is taken, and if that’s the case, we throw an exception. Otherwise, we save the user and send a notification e-mail.
Positive & Negative Scenario
Following, let’s do something different compared to other content about MockK. Let’s start by taking a look at the final result, and then we will see how we can get there.
So, firstly, let’s add the negative scenario:
class AwesomeMockkTest { private val userRepository: UserRepository = mockk() private val emailService = mockk<EmailService>() private val service = UserService(userRepository, emailService) @Test fun `should throw IllegalArgumentException when user with given e-mail already exists`() { val email = "contact@codersee.com" val password = "pwd" val foundUser = User(UUID.randomUUID(), email, password) every { userRepository.findUserByEmail(email) } returns foundUser assertThrows<IllegalArgumentException> { service.createUser(email, password) } verifyAll { userRepository.findUserByEmail(email) } } }
As we can see, we start all by defining dependencies for UserService
as MockK mocks, and then we simply inject them through the constructor.
After that, we add our JUnit 5 test that asserts if the createUser
function has thrown an exception, nothing unusual. The “unusual” part here is the every and verifyAll – those two blocks come from MockK, and we will get back to them in a moment.
With that done, let’s add one more test:
@Test fun `should return UUID when user with given e-mail successfully created`() { val email = "contact@codersee.com" val password = "pwd" val createdUserUUID = UUID.randomUUID() every { userRepository.findUserByEmail(email) } returns null every { userRepository.saveUser(email, password) } returns createdUserUUID every { emailService.sendEmail( to = email, subject = "Welcome to Codersee!", body = "Welcome user: $createdUserUUID." ) } just runs val result = service.createUser(email, password) assertEquals(createdUserUUID, result) verifyAll { userRepository.findUserByEmail(email) userRepository.saveUser(email, password) emailService.sendEmail( to = email, subject = "Welcome to Codersee!", body = "Welcome user: $createdUserUUID." ) } }
This time, we test the positive scenario, in which the user was not found by the e-mail
and was created successfully.
Defining MockK Mocks
With all of that done, let’s start breaking down things here.
Let’s take a look at what we did first:
private val userRepository: UserRepository = mockk() private val emailService = mockk<EmailService>() private val service = UserService(userRepository, emailService)
So, one of the approaches to define objects as mocks with MockK is by using the mockk
function.
It is a generic function. So that’s why I presented both ways to invoke it. But please treat that as an example. In real life, I suggest you stick to either mockk()
or mockk<EmailService>()
for a cleaner code.
Alternatively, we can achieve exactly the same with MockK annotations:
@ExtendWith(MockKExtension::class) class AwesomeMockkTest { @MockK lateinit var userRepository: UserRepository @MockK lateinit var emailService: EmailService @InjectMockKs lateinit var service: UserService }
The preferred approach is totally up to you. The important thing to mention is that the @ExtendWith(MockKExtension::class)
comes from JUnit 5.
And for the JUnit 4, we would implement a rule:
class AwesomeMockkTest { @get:Rule val mockkRule = MockKRule(this) @MockK lateinit var userRepository: UserRepository @MockK lateinit var emailService: EmailService @InjectMockKs lateinit var service: UserService }
Missing Stubbing
At this point, we know that we don’t use real objects, but mocks.
Let’s try to run our test without defining any behavior:
@Test fun `should throw IllegalArgumentException when user with given e-mail already exists`() { val email = "contact@codersee.com" val password = "pwd" assertThrows<IllegalArgumentException> { service.createUser(email, password) } verifyAll { userRepository.findUserByEmail(email) } }
In the console log, we should see the following:
Unexpected exception type thrown, expected: <java.lang.IllegalArgumentException> but was: <io.mockk.MockKException>
Expected :class java.lang.IllegalArgumentException
Actual :class io.mockk.MockKException
Well, the issue is that when we do not specify a stubbing for a function that was invoked, MockK throws an exception.
But our test logic looks already for exception, so that’s why we got a message that this is simply an unexpected one.
Without the assertThrows
, the message would be more obvious:
no answer found for UserRepository(#1).findUserByEmail(contact@codersee.com) among the configured answers: (UserRepository(#1).saveUser(eq(contact@codersee.com), eq(pwd))))
io.mockk.MockKException: no answer found for UserRepository(#1).findUserByEmail(contact@codersee.com) among the configured answers: (UserRepository(#1).saveUser(eq(contact@codersee.com), eq(pwd))))
So, lesson one: whenever we see a similar message, it means that our mock function was invoked, but we haven’t defined any stubbing.
Stubbing in MockK
And we already saw how we can define a stubbing, but let’s take a look once again:
val foundUser = User(UUID.randomUUID(), email, password) every { userRepository.findUserByEmail(email) } returns foundUser
We can read the above function as “return foundUser
every time the findUserByEmail
function is invoked with this, specific email value”.
When we run the test now, everything is working fine. Because in our logic, if the findUserByEmail
returns a not null value, an exception is thrown. So, no more functions are invoked. In other words, no more interactions with our mock object😉 Also, our email value matches.
And most of the time, this is how I would suggest defining stubbing. This way, we also make sure that the exact value of email
is passed.
When it comes to the answer part, returns foundUser
, we can also use alternatives, like:
answers { code }
– to define a block of code to run (and/or return a value)throws ex
– to throw exception- and many, many more (I will put a link to the docs at the end of this article)
MockK Matchers
The above stubbing expects that the email
value will be equal to what we define.
Technically, it is the equivalent of using the eq
function:
every { userRepository.findUserByEmail(eq(email)) } returns foundUser
And eq
uses the deepEquals
function to compare the values.
But sometimes, we do not want to, or we cannot define the exact value to match.
Let’s imagine that some function is invoked 20 times with various values. Technically, we could define all 20 matches.
But instead, we use one of the many matchers available in MockK, like any
:
every { userRepository.findUserByEmail(any()) } returns foundUser
And whereas eq
is the most concrete one, any
is the most generic one. The foundUser
is returned if findUserByEmail
is invoked with anything.
And MockK comes with plenty of other matchers, like:
any(Class)
– to match only if an instance of a given Class is passedisNull
/isNull(inverse=true)
– for null checkcmpEq(value)
,more(value)
,less(value)
– for thecompareTo
comparisons- and many more that you can find in the documentation
For example, let’s take a look at the match
example:
every { userRepository.findUserByEmail( match { it.contains("@") } ) } returns foundUser
As we can see, this way the foundUser
will be returned only if the passed value contains @
sign.
Unit Functions
At this point, we know how to deal with matchers and the returns
.
But what if the function does not return anything? We saw that previously, so let’s take a look once again:
every { emailService.sendEmail(any(), any(), any()) } just runs
Matchers are irrelevant right now, so I replaced them with any()
.
The important thing here is that just runs
is one of the approaches.
Alternatively, we can achieve the same:
every { emailService.sendEmail(any(), any(), any()) } returns Unit every { emailService.sendEmail(any(), any(), any()) } answers { } justRun { emailService.sendEmail(any(), any(), any()) }
The choice here is totally up to you.
Summary
And that’s all for our first lesson dedicated to MockK and Kotlin.
As I mentioned above, right here you can find the MockK documentation. And although I strongly encourage you to visit it, I also would suggest doing it after my series- when we cover the most important concepts:
- 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]
3 Responses
Why just don’t follow SOLID (and especially “Interface segregation” principle) to keep the code easy testable?
Fakes (and wrappers for 3rd party deps) extremely easy to do and they works a way faster then all those mocking libs. Again, no need to learn more API and libs and depend on them.
I would debate the overall benefit of doing that on our own if we gather together all aspects, not only performance. But if it works for you / in your environment, then you should stick to that.
I am the last person to promote anything as one-size-fits-all.
Nevertheless, you must remember that even if we would agree that custom fakes/wrappers are always the way to go, we don’t always choose the stack, or have the impact on the approach taken in the project 😉
Is MockK available for KMP?