Mocking types

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 helper chapter.

Only interfaces and abstract classes can be mocked! A mock is a generated implementation of its type, so a type that cannot be implemented (a concrete class, an object, an enum) cannot be mocked. This is by design and is not likely to change.

Requesting generation

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

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

// and

@UsesMocks(Database::class, API::class)
fun testDatabase() {}

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

Mocking abstract classes

An abstract class is mocked exactly like an interface, save for two differences:

  • Only its abstract members are mocked. A concrete member is left alone: it runs its real implementation, and can neither be given a behaviour with every nor be verified.

  • The generated mock extends it, so its constructor runs, with a faked value for each of its parameters.

abstract class Repository(val name: String) {
    abstract fun count(): Int
    fun describe(): String = "$name repository" (1)
}

@UsesMocks(Repository::class)
class MyTests {
    @Test fun test() {
        val mocker = Mocker()
        val repository = mocker.mock<Repository>()

        mocker.every { repository.count() } returns 42
        assertEquals(" repository", repository.describe()) (2)
    }
}
1 Not abstract, so it is not mocked: it runs, over the constructor arguments the mock was built with.
2 name is a faked String, so it is empty.
This is the reverse of an interface’s default method, which is overridden and mocked, and therefore must be given a behaviour like any other function.

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 DatabaseTests {
    @Test fun test() {
        val mocker = Mocker()
        val db = mocker.mock<Database>()
        val api = mocker.mock<API>()

        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.
A method must be mocked to run without throwing an exception (there is no "relaxed" mode).

A Mocker, and every mock created from it, must be used from a single thread. Calls arriving concurrently corrupt the mocker’s state instead of failing cleanly: calls go missing or land out of order, and a later verify fails for reasons unrelated to the code under test.

If the code under test dispatches, inject the dispatcher it uses so that the mocked calls still run on the test thread, rather than letting it reach a real background one.

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 fake<User>()
// Do things...
everyApiGetUserById42 returns null
// Do other things...

Mocking properties

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

@UsesMocks(Place::class)
class MyTests {
    @Test fun myUnitTest() {
        val mocker = Mocker()
        val place = mocker.mock<Place>()

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

        // Mocking a var property:
        mocker.every { place.name = isAny() } returns Unit (1)
        mocker.every { place.name } returns "Machu Pichu"
    }
}
1 A setter always returns Unit.

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

@UsesMocks(Place::class)
class MyTests {
    @Test fun myUnitTest() {
        val mocker = Mocker()
        val place = mocker.mock<Place>()

        mocker.backProperty(place, Place::name, default = "")

        place.name = "Machu Pichu"
        assertEquals("Machu Pichu", place.name)
    }
}

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.

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

mocker.every { api.getUserById(42) } returns fake<User>()

is strictly equivalent to:

mocker.every { api.getUserById(isEqual(42)) } returns fake<User>()

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 have been run in order with verify.

val fakeUser = fake<User>()

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:

val fakeUser = fake<User>()

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

//...

mocker.verify {
    called { api.getUserById(isAny()) }
}
1 The block is an ordinary lambda, not an inline one: its last expression is the returned value. A return here would try to return from the enclosing function and will not compile.

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 = mocker.mock<Delegate>()
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 = mocker.mock<Delegate>()
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) (1)
mocker.every { callback(isAny()) } returns Unit

userRepository.fetchUser(callback)

mocker.verify { callback(fakeUser) }
1 The mocker is mandatory: it is what records the call, exactly as it does for a mocked interface. The declared type of the property is what tells mockFunction1 the argument and return types, so there is nothing else to pass.

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(mocker) {} // implicit Unit

You can mock suspending functions with mockSuspendFunctionX, which works the same way. Note that a suspending function mock must be defined with everySuspending and checked with verifyWithSuspend, just like a suspending method:

val onUserLoaded: suspend (User) -> Unit = mockSuspendFunction1(mocker) (1)
mocker.everySuspending { onUserLoaded(isAny()) } returns Unit

userRepository.observeUser(onUserLoaded)

mocker.verifyWithSuspend { onUserLoaded(fakeUser) }
1 Creating the mock is not itself a suspending operation, so this line does not need to be in a coroutine.

Defining custom argument constraints

You can define your own constraints using isValid:

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 and in verifications:

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