Mocking interfaces
This section covers the use of the MocKMP |
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. |
|
Adding argument constraints
Available constraints are:
-
isAny
is always valid (even withnull
values). -
isNull
andisNotNull
check nullability. -
isEqual
andisNotEqual
check regular equality. -
isSame
andisNotSame
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:
…and needs to be replaced by:
|
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()) }