Mocking interfaces

This section covers the use of the MocKMP mocker by itself.
MocKMP also provides a very useful abstract class helper for test classes. The TestWithMocks helper class usage is recommended when possible (as it makes your tests easier to read), and is documented later in The test class helper chapter.

Only interfaces can be mocked!

Requesting generation

You can declare that a class needs a specific mocked interface by using the @UsesMocks annotation.

@UsesMocks(Database::class, API::class)
class MyTests

Once a type appears in @UsesMocks, the processor will generate a mock class for it.

Defining behaviour

To manipulate a mocked type, you need a Mocker. You can then create mocked types and define their behaviour:

@UsesMocks(Database::class, API::class)
class MyTests {
    @Test fun myUnitTest() {
        val mocker = Mocker()
        val db = MockDatabase(mocker)
        val api = MockAPI(mocker)

        mocker.every { db.open(isAny()) } returns Unit (1)
        mocker.every { api.getCurrentUser() } runs { fakeUser() } (2)
    }
}
1 returns mocks the method to return the provided instance.
2 runs mocks the method to run and return the result of the provided function.

Note that a method must be mocked to run without throwing an exception (there is no "relaxed" mode).

You can mock methods according to specific argument constraints:

mocker.every { api.update(isNotNull()) } returns true
mocker.every { api.update(isNull()) } runs { nullCounter++ ; false }

You can also keep the Every reference to change the behaviour over time:

val everyApiGetUserById42 = mocker.every { api.getUserById(42) }
everyApiGetUserById42 returns fakeUser()
// Do things...
everyApiGetUserById42 returns null
// Do other things...

Mocking properties

You can mock property getters & setters much like regular methods:

@UsesMocks(User::class)
class MyTests {
    @Test fun myUnitTest() {
        val mocker = Mocker()
        val user = MockUser(mocker)

        // Mocking a val property:
        mocker.every { user.id } returns 1

        // Mocking a var property:
        mocker.every { user.name = isAny() } returns Unit (1)
        mocker.every { user.name } returns "John Doe"
    }
}
1 A setter always returns Unit.

A var (read & write) property can be "backed" by the mocker:

@UsesMocks(User::class)
class MyTests {
    @Test fun myUnitTest() {
        val mocker = Mocker()
        val user = MockUser(mocker)

        mocker.backProperty(user, User::rwString, default = "")
    }
}

Defining suspending behaviour

You can define the behaviour of a suspending function with everySuspending:

mocker.everySuspending { app.openDB() } runs { openTestDB() } (1)
mocker.everySuspending { api.getCurrentUser() } returns fakeUser()
1 Here, openTestDB can be suspending.
  • You must use every to mock non suspending functions.

  • You must use everySuspending to mock suspending functions.

Adding argument constraints

Available constraints are:

  • isAny is always valid (even with null values).

  • isNull and isNotNull check nullability.

  • isEqual and isNotEqual check regular equality.

  • isSame and isNotSame check identity.

  • isInstanceOf checks type.

Note that passing a non-constraint value to the function is equivalent to passing isEqual(value)

mocker.every { api.getUserById(42) } returns fakeUser()

is strictly equivalent to:

mocker.every { api.getUserById(isEqual(42)) } returns fakeUser()

You cannot mix constraints & non-constraint values. This fails:

mocker.every { api.registerCallback(42, isAny()) } returns Unit

…​and needs to be replaced by:

mocker.every { api.registerCallback(isEqual(42), isAny()) } returns Unit

Verifying

You can check that mock functions has been run in order with verify.

val fakeUser = fakeUser()

mocker.every { db.loadUser(isAny()) } returns null
mocker.every { db.saveUser(isAny()) } returns Unit
mocker.every { api.getUserById(isAny()) } returns fakeUser

controller.onClickUser(userId = 42)

mocker.verify {
    db.loadUser(42)
    api.getUserById(42)
    db.saveUser(fakeUser)
}

You can of course use constraints (in fact, not using passing a constraint is equivalent to passing isEqual(value)):

mocker.verify {
    api.getUserById(isAny())
    db.saveUser(isNotNull())
}
You cannot mix constraints & non-constraint values.

If you want to verify the use of suspend functions, you can use verifyWithSuspend:

mocker.verifyWithSuspend {
    api.getUserById(isAny())
    db.saveUser(isNotNull())
}
You can check suspending and non suspending functions in verifyWithSuspend. Unlike everySuspending, all verifyWithSuspend does is running verify in a suspending context, which works for both regular and suspending functions.

Verifying exception thrown

If you define a mock function behaviour to throw an exception, you must verify the call with threw:

mocker.every { db.saveUser(isAny()) } runs { error("DB is not accessible") }

//...

mocker.verify {
    val ex = threw<IllegalStateException> { db.saveUser(isAny()) }
    assertEquals("DB is not accessible", ex.message)
}

If you configure your behaviour to maybe throw an exception, you can verify a call that may or may not have thrown an exception with called:

mocker.every { api.getUserById(isAny()) } runs { args ->
    val idArg = args[0] as Int
    if (idArg == 42) return MockUser()
    else throw UnknownUserException(idArg)
}

//...

mocker.verify {
    called { api.getUserById(isAny()) }
}

Configuring verification exhaustivity & order

By default, the verify block is exhaustive and in order: it must list all mocked functions that were called, in order. This means that you can easily check that no mocked methods were run:

mocker.verify {}

You can use clearCalls to clear the call log, in order to only verify for future method calls:

controller.onClickUser(userId = 42)
mocker.clearCalls() (1)

controller.onClickDelete()
mocker.verify { db.deleteUser(42) }
1 All mocked calls before this won’t be verified.

You can verify with:

  • exhaustive = false, which will verify each call, in their relative order, but won’t fail if you didn’t mention every call.

  • inOrder = false, which allows you to define all calls in any order, but will fail if you did not mention all of them.

  • exhaustive = false, inOrder = false, which checks required calls without order nor exhaustiveness.

mocker.verify(exhaustive = false, inOrder = false) { (1)
    db.deleteUser(42)
    api.deleteUser(42)
}
1 Verify that both calls have been made, no matter the order. Other calls to mocks may have been made since exhaustiveness is not checked.

Capturing arguments

You can capture an argument into a MutableList to use or verify it later. This can be useful, for example, to capture delegates and call them.

val delegate = MockDelegate()
mocker.every { delegate.setSession(isAny()) } returns Unit

val controller = Controller(delegate)
controller.startNewSession()
assertEquals(1, controller.runningSessions.size)

val sessionCapture = ArrayList<Session>()
mocker.verify { delegate.setSession(isAny(capture = sessionCapture)) } (1)

val session = sessionCapture.single() (2)
session.close()

assertEquals(0, controller.runningSessions.size)
1 Captures the setSession first argument into the sessionCapture mutable list.
2 As setSession should have been called only once, retrieve the one and only Session from the capture list.

Captures can also be used in definition blocks. The previous example could be rewritten as such:

val delegate = MockDelegate()
val sessionCapture = ArrayList<Session>()
mocker.every { delegate.setSession(isAny(capture = sessionCapture)) } returns Unit

val controller = Controller(delegate)
controller.startNewSession()
assertEquals(1, controller.runningSessions.size)

val session = sessionCapture.single()
session.close()

assertEquals(0, controller.runningSessions.size)

Note that, when declared in a definition block, the capture list may be filled with multiple values (one per call).

Accessing run block arguments

There are 2 ways you can access arguments in a run block.

  • You can use capture lists:

    val sessions = ArrayList<String>()
    mocker
        .every { delegate.setSession(isAny(capture = sessions)) }
        .runs { sessions.last().close() } (1)
    1 .last() returns the last call argument, which is always the current.
  • You can access function parameters in a run block arguments. This is less precise than using capture lists as they are non typed, but allows to write very concise code:

mocker
    .every { delegate.setSession(isAny()) }
    .runs { args -> (args[0] as Session).close() }

Mocking functional types

You can create mocks for functional type by using mockFunctionX where X is the number of arguments.

val callback: (User) -> Unit = mockFunction1()
mocker.every { callback(isAny()) } returns Unit

userRepository.fetchUser(callback)

mocker.verify { callback(fakeUser) }

The mockFunctionX builders can accept a lambda parameter that defines behaviour & return type of the mocked function (so that you don’t have to call mocker.every). The above mocked callback function can be declared as such:

val callback: (User) -> Unit = mockFunction1() {} // implicit Unit

You can mock suspending functions with mockSuspendFunctionX.

Defining custom argument constraints

You can define your own constraints:

fun ArgConstraintsBuilder.isStrictlyPositive(capture: MutableList<Int>? = null): Int =
    isValid(ArgConstraint(capture, { "isStrictlyPositive" }) {
        if (it > 0) ArgConstraint.Result.Success
        else ArgConstraint.Result.Failure { "Expected a strictly positive value, got $it" }
    })

You can then use your custom constraints in definitions or in verifications:

mocker.every { api.getSuccess(isStrictlyPositive()) } returns true
mocker.every { api.getSuccess(isAny()) } returns false
/*...*/
mocker.verify { api.getUserById(isStrictlyPositive()) }