A set of noteworthy uses of MockK (that might come in handy)
Christiaan Schop - Mar 6, 2023
As a Java developer I was used to Mockito and during this time it helped me a lot. It is a library that has been there for us for a long time. Sometimes you added Powermockito to give Mockito some extra 'juice' to overcome some final, static or more private issues, but helpful it is. Having said that, last year I started doing some Kotlin work and I quickly came to know MockK. The use of Unit
and the fact that classes/methods are final require that juice even more often in Kotlin. I believe MockK is the answer to just that, so I took out some noteworthy uses of MockK. Hope you enjoy!
Mocking... everything
To serve you an amuse, first a short introduction of how to think about MockK. Maybe the best thing to tell you about what can be mocked with MockK is; for almost everything you can think of is probably a very specific solution created. A simple mock looks a lot like Mockito with; @MockK
or mock<>()
. However, what about an Object
(singleton), Class
, Constructor
, Enum
? Just start typing in your IDE and you will probably find a one-lined MockK solution quickly. Also, the MockK website has many examples.
Quick example of the Enumeration as the least straightforward mockk (in my opinion). Here you use the mockkObject
singleton mock of an Object
to initiate the Enum mock.
1 2 3mockkObject(HeavyMetalStrengthEnum.TUNGSTEN) every { HeavyMetalEnum.TUNGSTEN.strenght } returns 999 assertEquals(1, HeavyMetalStrengthEnum.TUNGSTEN.strenght)
InjectMockKs
As a starter, I have chosen a rather basic code example. We can note that our Machine()
will automatically detect the @MockK
properties by using the @InjectMockKs
in combination with the @ExtendWith(MockKExtension::class)
annotation. It is designed to look for properties in following order: name -> class -> superclass.
If we change the @InjectMockKs
, which only mocks lateinit var
& var
by default, to @OverrideMockKs
, we also override val
properties
1 2 3 4 5 6 7 8 9 10 11 12 13class Machine { lateinit var filterOne: MachineFilter lateinit var filterTwo: MachineFilter } @ExtendWith(MockKExtension::class) class MachineTest { @MockK lateinit var filterOne: MachineFilter @MockK lateinit var filterTwo: MachineFilter @InjectMockKs var machine = Machine() }
Stubbing, but more 'relaxed'
In this test we mock an interface in three ways, showing how a mockk<>()
can easily be turned into a stub with default return values. It makes you able to focus only on the behavior you want to specify and focus on. First we see that just a mockk<>()
will throw exceptions in both methods in the interface. However, if we add a config relaxed = true
to the second mock, both methods run perfectly fine, returning an empty String
and Unit
respectively. There is also the option to only stub methods returning Unit
by adding the config relaxUnitFun = true
, which is shown in the last mock.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16interface IFoundryLetsDoThings { fun doStuffAndReturnString(cool: String): String fun doStuffAndReturnUnit(): Unit //`Unit` is not necessary, but to be clear } val foundryMock = mockk<IFoundryLetsDoThings>() assertThrows<MockKException> { foundryMock.doStuff() } //Throws exception assertThrows<MockKException> { foundryMock.doUnitThings() } //Throws exception val foundryStub = mockk<IFoundryLetsDoThings>(relaxed = true) foundryStub.doStuff("thing") foundryStub.doUnitThings() val foundryStubUnit = mockk<IFoundryLetsDoThings>(relaxUnitFun = true) assertThrows<MockKException> { foundryMock.doStuff("thing") } //Throws exception foundryStub.doUnitThings()
Oh so original
Sometimes you might need that only one method is called for real and all others should be stubbed. In the below example we see just that. We mock a class Metal
and stub its method, but also tell that for one specific value the original call should be made.
1 2 3 4 5 6 7 8 9class Metal { fun filterAluminium(name: String): String = if (name.contains("alu")) "found it!" else "nah" } val metal = mockk<Metal>() every { metal.filterAluminium(any()) } returns "no decision" every { metal.filterAluminium("aluminium") } answers { callOriginal() } assertEquals("no decision", metal.filterAluminium("alu")) assertEquals("found it!", metal.filterAluminium("aluminium"))
Capturing machine(s)
In the next examples we see MockK's Capture
at work (in Mockito this is ArgumentCaptor
), capturing arguments. The subject here is a MachineService
that injects a MachineRepository
to save a machine. A common scenario, where a service executes, and you want to check what is processed into e.g. repository or third party library.
In following example has two things to mention. The slot
, which is used as an argument in capture
, which in its turn is the argument in the save
method of the repository. The other one being that we return the result by calling slot.captured
1 2 3 4 5 6 7 8 9 10 11class MachineService(val repository: MachineRepository) { fun saveMachine(machine: String) = repository.save(machine) } val slot = slot<String>() val machine = "capture me please I am a machine" every { repository.save(capture(slot)) } returns Unit service.saveMachine(machine) assertEquals(machine, slot.captured)
You can also catch multiple arguments in one test, which is shown in the next example. The only change to make is that the capture
does not receive a slot
, but a mutableListOf
.
As a small extra I have changed returns Unit
to just runs
which is quite handy when you do not care about the Unit it returns and want to be explicit about it.
[HINT] If we had a larger list of methods that required stubbing, we now could know a neat answer for this. Rather listing methods with just runs
we could also use the mockk<type>(relaxed = true)
1 2 3 4 5 6 7 8 9val list = mutableListOf<String>() val machine = "capture me please I am a machine" every { repository.save(capture(list)) } just runs machineService.saveMachine(machine) machineService.saveMachine(machine) assertEquals(machine, list[0]) assertEquals(machine, list[1])
Coroutines
With Kotlin coroutines and project Loom peeking around the corner I definitely wanted a coroutine MockK example for dessert.
We created a FoundryService
with a repository and a cache in its constructor. We use suspend
functions, which can only live in a coroutine. We use the faker lib to create us some quality mock data and initialize the service. With coEvery
we can instruct co
routine mocks and tell it to return the foundry
. We start the main function inside a runBlocking
coroutine scope. In my opinion, blocking is best practice when it comes to unit testing, it mitigates the asynchronous of things. Making it more reliable and often less flaky in pipelines. Lastly we can verify suspended results with coVerify
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24class FoundryService( private val foundryCache: FoundryCache, private val foundryRepository: FoundryRepository ) { suspend fun getCurrentCustomer(): Foundry { val company = foundryCache.getCurrentCompany() //suspend function return foundryRepository.getFoundryByName(company.name) //suspend function } } val faker = Faker() val foundry = Foundry( name = faker.company.name(), //Effertz, Heathcote and Goyette address = faker.address.fullAddress() //Suite 884 55298 Olin Lodge, Davisburgh, CT 69233 ) val foundryCache: FoundryCache = mockk() val foundryRepository: FoundryRepository = mockk() val service = FoundryService(foundryCache, foundryRepository) coEvery { foundryCache.getCurrentCompany() } returns foundry runBlocking { //Coroutine scope service.getCurrentCustomer() } coVerify { foundryRepository.getFoundryByName(foundry.name) }
[Hint] If you like the faker library as much as I do, you could read the Spice up your tests using Faker blog!
Final remarks
Whereas tests in general help to understand code and frameworks quicker, neat and easy to understand tests increase this process. MockK definitely tries to help us with this. I feel like the syntax can be interpreted more naturally and with the right variables it just flows, almost like complete sentences.
Although the examples could have been much juicier with exotic examples, I hope these uses are readily applicable to your work with Kotlin tests.
Thanks for reading!