Getting Started with MockK in Kotlin [1/5]

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!

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 above, we can see both ways in which we can invoke it. It is just an example, and 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) }
}

Well, we would get something like:

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 passed

isNull / isNull(inverse=true) – for null check

cmpEq(value) , more(value) , less(value) – for the compareTo 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]

The post Getting Started with MockK in Kotlin [1/5] appeared first on Codersee — Kotlin on the backend.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *