MockK unit test

Posted by agge on Wed, 24 Nov 2021 16:41:42 +0100

What is unit testing?

A unit test is a piece of automated code that calls the unit of work being tested and then tests some assumptions about the single final result of the unit.

Definition of Mock

mock is to create a false object of a class and replace the real object in the test environment to achieve two purposes:

  • 1. Verify the calling of some methods of this object, how many times they have been called, what the parameters are, etc
  • 2. Specify the behavior of some methods of this object, return specific values, or perform specific actions

Introduction to MockK

mockk is a Mocking framework written by Kotlin. Through mockk < > (), the object returned by mockkobject(), spyk(), is in the mock state. Only objects in this state can mock the behavior of objects through every{}. mockk framework follows the process of mock - Listen - execute - verify.

import io.mockk.*

for instance

public class ServiceImplA implements Service {
    @Override
public void doSomething1(String s) {
    log.info("input is:{}", s);
}

@Override
public String doSomething2(String s) {
    log.info("doSomething2,input:{}", s);
    return s;
}
}

mock common object

Mock an object with the statement mock (...)
This method returns an instance of T. all functions of this instance are in the state of pending mock. These functions in the state of pending mock cannot be called directly. They can be called only after combining the corresponding method of mock with the every {} statement

//Returns a mock object for ServiceImplA
val mockk = mockk<ServiceImplA>()
//mock specifies the method
every { mockk.doSomething1(any()) } returns Unit
//Call the method that is mock
mockk.doSomething1("")
//If this method fails to mock through every, an error will be reported
mockk.doSomething2("")

every {...} statement is used to listen to the specified code statement and take the following actions, for example:

  • return value returns a value
  • just Runs continues execution (for Unit methods only)
  • answer {} executes a statement block

When the method in every {...} is executed in the test code, it does not really execute, but directly returns the object after returns. You can also skip execution using every {} just runs without returns.

mockk Object class

Turns the specified object into a mock able state

The difference from mockk < > () is that the returned mock object allows the mock behavior to coexist with the real behavior. If you do not actively mock, the real behavior will be executed.

val serviceImplA = ServiceImplA()
    mockkObject(serviceImplA)
    every { serviceImplA.doSomething1(any()) } returns Unit
    //Called mock method
    serviceImplA.doSomething1("sfas")
    //Call real method
    serviceImplA.doSomething2("sfas")

If you want to verify and execute the private methods in the object class, you need to specify a value recordPrivateCalls during mock, which is false by default:

mockkObject(serviceImplA, recordPrivateCalls = true)

mock static class

mockkStatic(serviceImplA::class)

spyk() & spyk(T obj)

Returns the spyk object of T or the spyk object of obj
The difference from mockk < > () is that the object returned by spyk < > () allows real behavior to coexist with mock behavior, and its performance is similar to mockobject ()

    //Returns a spyk object of ServiceImplA
    val spyk = spyk<ServiceImplA>()
    every { spyk.doSomething1(any()) } returns Unit
    //Call mock method
    spyk.doSomething1("123")
    //Call real method
    spyk.doSomething2("999")
val serviceImplA = ServiceImplA()
//Returns the object after the serviceImplA object is spyk. The original object will not be changed
val spyk1 = spyk(serviceImplA)
//serviceImplA is not in mock status. An error will be reported here
//every { serviceImplA.doSomething1(any()) } returns Unit

//mock
every { spyk1.doSomething1(any()) } returns Unit
//Call mock method
spyk1.doSomething1("999")
//Call real method
spyk1.doSomething2("999")

returns

Action is the result of custom mock behavior

val spyk = spyk<ServiceImplA>()
//Mock dosomething 2, returns 111 no matter what input
every { spyk.doSomething2(any()) } returns "111"

val input = "222"
//What I got here should be 111
val mockkResult = spyk.doSomething2(input)
println("mockk Behavior results:$mockkResult")

val real = ServiceImplA()
//You should get 222 here
val realResult = real.doSomething2(input)
println("mockk Behavior results:$realResult")

Verify that multiple methods are called

If you want to verify that the two methods are executed, you can verify both methods in verify {...}. If the confirmation is successful, the test passes, otherwise an error is reported.

@Test
fun test() {
     //Returns a mock object for ServiceImplA
     val mockk = mockk<ServiceImplA>()
    //mock specifies the method to set listening
    every { mockk.doSomething1(any()) } returns Unit
    every { mockk.doSomething2(any()) } returns Unit
    verify { 
        mockk.doSomething1("sfas")
        mockk.doSomething2("sfas")
    }
}

Assign a default behavior to a method that has no return value

By replacing the Returns after every {...} with just Runs, MockK can assign a default behavior to the method with no return value.

@Test
fun test() {
    val serviceImplA = mockk<ServiceImplA>()
    every { serviceImplA.doSomething1(any()) } just Runs
    verify { serviceImplA.doSomething1(any()) }
}

The number of times the validation method was called

If you want to verify not only that the method is called, but also the number of times the method is called, you can specify the exatcly, atLeast, and atMost properties in verify.

  // Validation called twice
    verify(exactly = 2) { serviceImplA.doSomething1(any()) }
  
    // Validation was called at least once
    // verify(atLeast = 1) { serviceImplA.doSomething1(any()) }
  
    // Validation was called up to two times
    // verify(atMost = 2) { serviceImplA.doSomething1(any()) }

Verify that the Mock methods have been called

verifyAll {
        serviceImplA.doSomething1(any())
        serviceImplA.doSomething2(any())
        serviceImplA.doSomething3(any())
        serviceImplA.doSomething4(any())
    }

Verify that all Mock methods are called in a specific order

If you not only want to test that several methods have been called, but also want to ensure that they are called in a fixed order, you can use verifySequence {...}.

verifySequence {
       serviceImplA.doSomething1(any())
        serviceImplA.doSomething2(any())
        serviceImplA.doSomething3(any())
        serviceImplA.doSomething4(any())
    }

Validate mock object private method

Verification is placed in verify {...} and verified by reflection:

verify{ mockClass["privateFunName"](arg1, arg2, ...) }

It is mainly divided into three parts:

1.mock class
2. Square brackets, filled with double quotation marks + private method name
3. In parentheses, fill in the transmitted parameters
Note: the object class of mock also needs to set recordPrivateCalls to true

Delayed verification

Delay verification can be realized by using verify(timeout) {...}. For example, timeout = 2000 in the following code means to check whether the method is called after 2 seconds.

 verify(timeout = 2000) { serviceImplA.doSomething1(any()) }

assertEquals

Judge whether the effectedNum is the same as the expected value 1. If it is different, test fail

assertEquals(expected:1,effectedNum)

relaxed

There is a Car under test, which depends on an Engine:

class Car(private val engine: Engine) {
fun getSpeed(): Int {
    return engine.getSpeed()
}
}

class Engine {
    fun getSpeed(): Int {
        return calSpeed()
    }
    private fun calSpeed(): Int {
        return 30
    }
}

To test getSpeed(), it depends on the methods in the Engine, so you need to mockk the Engine and write the following test methods:

fun testCar() {
    // mock engine object
    val engine = mockk<Engine>()
    val car = Car(engine)
    // Here is the writing method of private method setting listening:
    every { engine["calSpeed"]() } returns 30
    val speed = car.getSpeed()
    assertEquals(speed, 30)
}

However, an error is reported here: io.mockk.MockKException: no answer found for: Engine(#1).getSpeed()

This is because mockk strictly executes each method, and although Engine mocks it, mockk does not know whether Engine.getSpeed() needs to be executed further, so it throws an error.

At this time, you have three solutions.
Scheme 1: change the construction of engine from mock to spy, because spy can truly simulate the behavior of objects: engine = spyk()
Scheme 2: abandon the calSpeed method and use every {engine. Getspeed()} returns 30

Scheme 3: when mocking engine, set relaxed to true, engine = mockk(relaxed = true)