
Vor vier Jahren habe ich mich auf meinem Blog mit dem Java-Framework Mockito auseinandergesetzt. Mittlerweile arbeite ich bevorzugt mit Kotlin. Diese JVM-basierte Sprache hat aus den Entscheidungen von Java viele Schlüsse gezogen und macht einiges fundamental anders. Mockito existiert zwar auch für Kotlin, hat aber einige Einschränkungen. Für möglichst idiomatischen Mock-Code von Kotlin-Objekten und -Funktionen setze ich daher MockK ein.
In diesem Artikel gebe ich Einblicke in dieses Framework und zeige, wie schnell und effizient man mit MockK starten kann.
Der erste Mock
Die Funktion mockk
erzeugt eine Mock-Instanz einer Klasse. Hier offenbart sich der erste Unterschied zu Mockito, denn man muss kein Class-Objekt der zu mockenden Klasse übergeben. Stattdessen ist mockk
eine generische Funktion und der Typ mit dem Keyword reified
versehen, so dass die Typinformation zur Laufzeit erhalten bleibt und MockK damit die richtige Instanz zurückgeben kann.
val foo = mockk<File>()
val bar = mockk<String>(relaxed = true)
Der vorangegangene Code erzeugt zwei Mock-Objekte foo
und bar
, von denen das zweite mit dem Parameter relaxed
erzeugt wird und damit Default-Werte für alle seine Funktionen und Properties hinterlegt hat. Ist dies nicht der Fall, wie beim ersten Mock foo, muss jede aufgerufene Funktion explizit gemockt werden, da sonst eine Exception fliegt.
Analog zu Mockito existiert auch eine Annotation @MockK
, über die ein Mock erzeugt werden kann, mir gefällt die Instanzierung über die Funktion aber mehr, da dieser Schritt im Debugger besser nachvollziehbar ist.
Die Bibliothek springmockk integriert MockK mit dem Spring-Framework und stellt dafür unter anderem die Annotation @MockkBean
zur Verfügung.
Die Funktion spyk
kann eine vorhandene Instanz entgegennehmen oder genau wie mockk
auf Basis der Typinformation ein Spy-Objekt erzeugen.
Funktionen mocken
Während in Mockito bzw. BDDMockito die Methoden when
und given
existieren, wird in MockK die Funktion every
verwendet. Diese nimmt einen Block entgegen und ermöglicht über die Funktionen returns
, answers
und throws
das Verhalten beim Aufruf zu definieren.
class Foo {
fun greet(name: String) = "Hello, $name"
}
fun test() {
val mockReturns = mockk<Foo>()
every { mockReturns.greet(any())
} returns "Hello Mocked"
val mockAnswers = mockk<Foo>()
every { mockAnswers.greet(any())
} answers { "Hello ${firstArg<String>()}" }
val mockThrows = mockk<Foo>()
every { mockThrows.greet(any())
} throws IllegalArgumentException("mocked")
}
Der Code demonstriert die verschiedenen Möglichkeiten von Mocks. Die Funktion returns
erzeugt einen fixen Rückgabewert bei jedem Aufruf der Funktion. Mittels answers
und der Scope Function firstArg
wird beim zweiten Mock die Antwort dynamisch aus dem Argument beim Aufruf erzeugt. Die Funktion throws
sorgt dafür, dass jeder Aufruf zu einer Exception führt.
Hierarchische Mocks
Mocks können hierarchisch aufgebaut werden. Dies bietet sich an, wenn man die inneren Mocks nur einmal benötigt und sich daher weitere Zuweisungen sparen möchte, wie das folgende Beispiel demonstriert:
class Foo { fun foo() = Bar() }
class Bar { fun bar() = Baz() }
class Baz { fun baz() = "baz" }
fun test() {
val mock = mockk<Foo>()
every { mock.foo() } returns mockk {
every { bar() } returns mockk {
every { baz() } returns "bazMocked"
}
}
}
Interaktionen
Mit verify
und wasNot
kann sichergestellt werden, dass eine bestimmte Funktion aufgerufen oder mit einem Mock interagiert wurde.
class Foo {
fun bar() = "bar"
}
fun examples() {
val foo = mockk<Foo>()
verify { foo wasNot Called }
verify { foo.bar() }
verify(exactly = 0) { foo.bar() }
verify(exactly = 1) { foo.bar() }
verify(atLeast = 2) { foo.bar() }
verify(atMost = 3) { foo.bar() }
}
Der Code verifiziert in folgender Reihenfolge, dass:
- keine Interaktion mit dem Mock stattgefunden hat
bar
aufgerufen wurdebar
nicht aufgerufen wurdebar
genau einmal aufgerufen wurdebar
mindestens zweimal aufgerufen wurdebar
maximal dreimal aufgerufen wurde
MockK unterstützt aus Mockito bekannte Argument Matcher wie any
, eq
und isNull
. Mit der Funktion withArg
kann ein benutzerdefinierter Matcher als Block übergeben werden:
class Foo {
fun bar(baz: Map<Int, String>) =
baz.getOrDefault(0, "baz")
}
fun examples() {
val foo = mockk<Foo>()
verify {
foo.bar(
withArg { map -> assert(map[0] == "foo") }
)
}
}
Argumente erfassen
Slots in MockK sind das Pendant zu Argument Captors in Mockito. Der Umgang mit ihnen unterscheidet sich je nachdem, ob man genau einen Wert oder eine variable Anzahl erfassen möchte.
Um genau einen Wert je Argument zu erfassen, bietet sich die Funktion slot
an, die der Semantik von mockk
und spyk
folgt:
class Foo(val v: String)
fun bar(foo: Foo) = "bar"
fun test() {
val fooSlot = slot<Foo>()
verify { bar(capture(fooSlot)) }
bar(Foo("foo"))
assert(fooSlot.isCaptured)
assert(fooSlot.captured.v == "foo")
}
Die Alternative dazu, die eine beliebige Anzahl an Aufrufen erfassen kann, ist eine MutableList
:
class Foo(val v: String)
fun bar(foo: Foo) = "bar"
fun test() {
val fooSlot = mutableListOf<Foo>()
verify { bar(capture(fooSlot)) }
bar(Foo("foo1"))
bar(Foo("foo2"))
assert(fooSlot.size == 2)
assert(fooSlot[0].v == "foo1")
assert(fooSlot[1].v == "foo2")
}
Extension Functions, Objects und Top Level Functions
Extension Functions können mit MockK wie normale Funktionen gemockt werden:
fun String.foo() = "foo"
fun test() {
val stringMock = mockk<String>()
every { stringMock.foo() } returns "bar"
}
Singleton und Companion Objects können erst nach Aufruf von mockkObject
gemockt werden. Im Anschluss sollte unmockkObject
aufgerufen werden, da es sonst zu Fehlern kommen kann. Das folgende Beispiel verwendet die Variante von mockkObject
, die einen Block entgegennimmt und nach Abarbeitung desselbigen automatisch unmockkObject
aufruft:
object Foo {
fun foo() = "foo"
}
class Bar {
companion object {
fun bar() = "bar"
}
}
fun test() {
mockkObject(Foo) {
every { Foo.foo() } returns "fooMocked"
}
mockkObject(Bar) {
every { Bar.bar() } returns "barMocked"
}
}
Das Mocken von Top Level Functions folgt dem gleichen Schema. Die Funktionen heißen mockkStatic und unmockkStatic. Top Level Functions werden über zwei aufeinanderfolgende Doppelpunkte referenziert:
fun baz() = "baz"
fun main() {
mockkStatic(::baz) {
every { baz() } returns "bazMocked"
}
}
Fazit
MockK erlaubt es, in Kotlin unter Zuhilfenahme von Blöcken und Reifizierung idiomatische Mocks zu schreiben und ihr Verhalten dynamisch zu gestalten. Dieser Artikel hat einen ersten Einblick gegeben, was mit MockK möglich ist. Das Framework bietet darüber hinaus aber auch dedizierte Unterstützung für Koroutinen und viele weitere spannende Features.