1. Overview
MockK is a powerful mocking library for Kotlin testing. When it comes to stubbing functions with varargs, MockK provides elegant solutions.
In this tutorial, we’ll learn how to stub functions with a vararg parameter using MockK.
2. MockK’s Varargs Support
Let’s start by creating a basic function with a vararg parameter. This will allow us to demonstrate the process of stubbing a function and matching the vararg argument effectively:
class MyClass {
fun joinBySpace(vararg strings: String): String {
return strings.joinToString(separator = " ")
}
}
The joinBySpace() function is pretty straightforward. As the function name implies, it joins a variable number of String values using a space as the separator:
val result = MyClass().joinBySpace("a", "b", "c", "d", "e")
assertEquals("a b c d e", result)
From version 1.9.1, MockK has introduced powerful vararg matchers, offering flexible parameter-matching capabilities. We’ll delve into three key matchers: anyVararg(), varargAll(), and varargAny().
First, let’s initialize a mock for our MyClass:
val mockkObj = mockk<MyClass>()
*We’ll use the mockkObj object throughout this tutorial to stub joinBySpace().*
When using anyVararg(), varargAll(), or varargAny() to match a vararg argument, we adhere to a consistent pattern: FunctionToStub(optional prefix elements, *VarargMatcherFun(), optional suffix elements), where VarargMatcherFun() indicates matcher functions such as anyVararg(), varargAll(), etc.
Let’s see an example:
every{ mockkObj.joinBySpace("a", "b", "c", *anyVararg(), "x", "y", "z") } ...
In MockK’s vararg matching, both prefix and suffix elements are optional. However, if they’re provided, they contribute to the argument match checks. VarargMatcherFun() matches elements between prefix and suffix elements.
It’s important to note that all built-in vararg matchers are functions that return an array. Consequently, we use the spread operator (*) to ensure that they’re passed correctly as part of a vararg argument.
Next, let’s examine anyVararg(), varargAll(), and varargAny() matchers more closely.
3. The anyVararg() Matcher
The anyVararg() function doesn’t require any parameter. It serves to match any number of elements with any value within a vararg. An example can clearly show how it works:
every { mockkObj.joinBySpace("a", "b", "c", *anyVararg(), "x", "y", "z") } returns "Wow, Kotlin rocks!"
val result = mockkObj.joinBySpace("a", "b", "c", "Baeldung", "is", "cool", "x", "y", "z")
assertEquals("Wow, Kotlin rocks!", result)
val result2 = mockkObj.joinBySpace("a", "b", "c", "x", "y", "z")
assertEquals("Wow, Kotlin rocks!", result2)
4. The varargAll() Matcher
With varargAll(), we can specify a lambda to establish a condition that all vararg elements (excluding prefix and suffix) must satisfy. The lambda takes a vararg element as the input and returns a Boolean.
Next, let’s see an example:
every { mockkObj.joinBySpace("a", "b", "c", *varargAll { it.startsWith("d") }, "z") } returns "Wow, Kotlin rocks!"
val result = mockkObj.joinBySpace("a", "b", "c", "d1", "d2", "d-whatever", "z")
assertEquals("Wow, Kotlin rocks!", result)
In the code above, the matching rule defined with varargAll() ensures that all elements it covers start with the letter ‘d’. If any element deviates from this rule, the lambda returns false. Consequently, our stub will not be invoked for the following function call:
assertThrows<MockKException> {
mockkObj.joinBySpace("a", "b", "c", "d1", "Baeldung", "z")
}.also { exception ->
assertTrue(exception.message!!.startsWith("no answer found"))
}
In the above example, the value “Baeldung” doesn’t start with ‘b‘. So, this call doesn’t match our previously defined stub.
Furthermore, if varargAll() encompasses no elements, the function mock remains operational:
val result2 = mockkObj.joinBySpace("a", "b", "c", "z")
assertEquals("Wow, Kotlin rocks!", result2)
5. The varargAny() Matcher
The varargAny() function works similarly to varargAll() but with a distinct requirement: It requires that at least one element matches the condition specified in the lambda.
Next, let’s apply varargAny() in the previous example:
every { mockkObj.joinBySpace("a", "b", "c", *varargAny { it.startsWith("d") }, "z") } returns "Wow, Kotlin rocks!"
val result = mockkObj.joinBySpace("a", "b", "c", "d1", "d2", "d-whatever", "z")
assertEquals("Wow, Kotlin rocks!", result)
val result2 = mockkObj.joinBySpace("a", "b", "c", "d1", "Baeldung", "z")
assertEquals("Wow, Kotlin rocks!", result2)
This time, although “Baeldung” doesn’t start with ‘b‘, “d1” makes varargAny() return true. Therefore, our stub matches and gets called.
However, if varargAny() covers no elements, our stub won’t match:
assertThrows<MockKException> {
mockkObj.joinBySpace("a", "b", "c", "z")
}.also { exception ->
assertTrue(exception.message!!.startsWith("no answer found"))
}
6. MockKVarargScope‘s nArgs and position
We’ve discussed varargAny() and varargAll() functioning in a similar context. Both rely on a lambda to evaluate whether the respective elements fulfill certain criteria. Now, let’s delve into their function definitions:
inline fun <reified T : Any> varargAll(noinline matcher: MockKVarargScope.(T) -> Boolean)
inline fun <reified T : Any> varargAny(noinline matcher: MockKVarargScope.(T) -> Boolean)
As we can see, the matcher parameter isn’t just (T) -> Boolean, but rather MockKVarargScope.(T) -> Boolean. This implies that the lambda also functions as an extension function of the MockKVarargScope class, enabling it to use properties provided by MockKVarargScope.
Next, let’s look at the definition of the MockKVarargScope class:
class MockKVarargScope(val position: Int, val nArgs: Int)
The MockKVarargScope class defines two properties:
- position – Represents the index (zero-based) of the current element within the vararg argument
- nArgs – Denotes the total number of elements within the vararg argument
It’s important to emphasize that both position and nArgs properties encompass all elements, including those in the prefix and suffix.
Next, let’s see how to make use of these properties in varargAll() and varargAny().
6.1. The nArgs Property
We can directly use the nArgs property within the lambda expression to perform checks regarding the size of the vararg argument.
Next, let’s take varargAll() as an example to show how it works:
every { mockkObj.joinBySpace("a", "b", "c", *varargAll { nArgs > 6 }, "z") } returns "Wow, Kotlin rocks!"
val result = mockkObj.joinBySpace("a", "b", "c", "Baeldung", "is", "cool", "z")
assertEquals("Wow, Kotlin rocks!", result)
As the code shows, we created a stub for joinBySpace(). Further, it checks that if the vararg argument comprises a minimum of seven elements, it returns a predefined value.
Otherwise, the stub won’t get invoked:
assertThrows<MockKException> {
mockkObj.joinBySpace("a", "b", "c", "Baeldung", "z")
}.also { exception ->
assertTrue(exception.message!!.startsWith("no answer found"))
}
Since nArgs represents the count of elements in the vararg argument, varargAll { nArgs > 6 } and varargAny { nArgs > 6 } make no difference:
every { mockkObj.joinBySpace("a", "b", "c", *varargAny { nArgs > 6 }, "z") } returns "Wow, Kotlin rocks!"
val result = mockkObj.joinBySpace("a", "b", "c", "Baeldung", "is", "cool", "z")
assertEquals("Wow, Kotlin rocks!", result)
assertThrows<MockKException> {
mockkObj.joinBySpace("a", "b", "c", "Baeldung", "z")
}.also { exception ->
assertTrue(exception.message!!.startsWith("no answer found"))
}
6.2. The position Property
Similarly, the position property allows us to conduct an element-index-based check within the lambda passed to varargAll() and varargAny().
Again, let’s first take varargAll() as an example to demonstrate how to use position within the lambda:
every {
mockkObj.joinBySpace(
"a", "b", "c", *varargAll { if (position % 2 == 0) it == "E" else it == "O" }, "z"
)
} returns "Wow, Kotlin rocks!"
val result = mockkObj.joinBySpace("a", "b", "c", "O", "E", "O", "E", "z")
assertEquals("Wow, Kotlin rocks!", result)
In this example, we’ve specified that for the elements covered by varargAll(), if their position is even, the element should be “E“. Otherwise, an”O” element is required.
Since we used the varargAll() matcher, any element that breaks the specified rule results in an unmatched condition:
assertThrows<MockKException> {
mockkObj.joinBySpace("a", "b", "c", "Baeldung", "z")
}.also { exception ->
assertTrue(exception.message!!.startsWith("no answer found"))
}
assertThrows<MockKException> {
mockkObj.joinBySpace("a", "b", "c", "O", "Baeldung", "is", "cool", "z")
}.also { exception ->
assertTrue(exception.message!!.startsWith("no answer found"))
}
However, if we replace varargAll() with the varargAny() matcher in the example above, the varargAny() matcher considers it matched as long as at least one element conforms to the position rule:
every {
mockkObj.joinBySpace(
"a", "b", "c", *varargAny { if (position % 2 == 0) it == "E" else it == "O" }, "z"
)
} returns "Wow, Kotlin rocks!"
assertThrows<MockKException> {
mockkObj.joinBySpace("a", "b", "c", "Baeldung", "z")
}.also { exception ->
assertTrue(exception.message!!.startsWith("no answer found"))
}
val result = mockkObj.joinBySpace("a", "b", "c", "O", "Baeldung", "is", "cool", "z")
assertEquals("Wow, Kotlin rocks!", result)
7. Conclusion
In this article, we’ve delved into using MockK’s anyVararg(), varargAll(), and varargAny() matchers to create stubs for functions in mock objects. Additionally, we’ve showcased examples illustrating the utilization of MockKVarargScope‘s properties within varargAll() and varargAny().
As always, the complete source code for the examples is available on GitHub.