Mockative

Maven Central

Mocking for Kotlin/Native and Kotlin Multiplatform using the Kotlin Symbol Processing API (KSP).

Installation

Mockative uses KSP to generate mock classes for interfaces, and as such, it requires adding the
KSP plugin in addition to adding the runtime library and symbol processor dependencies.

build.gradle.kts

plugins {
    id("com.google.devtools.ksp")
}

repositories {
    mavenCentral()
}

kotlin {
    sourceSets {
        val commonTest by getting {
            dependencies {
                implementation("io.mockative:mockative:1.0.0")
            }
        }
    }
}

dependencies {
    ksp(implementation("io.mockative:mockative-processor:1.0.0"))
}

Installation for JVM projects

build.gradle.kts

plugins {
    id("com.google.devtools.ksp")
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("io.mockative:mockative-jvm:1.0.0")
    
    ksp(implementation("io.mockative:mockative-processor-jvm:1.0.0"))
}

Testing with Mockative

To mock a given method on an interface, annotate a property holding the interface type with the
@Mock annotation, and assign it to the result of a call to the <T> mock(KClass<T>) function:

class GitHubServiceTests {
    @Mock
    val api = mock(GitHubAPI::class)
}

Then, to stub a function or property on the mock there is a couple of options:

Stubbing using Expressions

It is possible to stub a function or property by invoking it through the use of either
the <R> invocation(T.() -> R) or <R> coroutine(T.() -> R) function available from
the <T> given(T) function:

// Stub a `suspend` function
given(mock).coroutine { fetchData("mockative/mockative") }
    .then { data }

// Stub a blocking function
given(mock).invocation { transformData(data) }
    .then { transformedData }

// Stub a property getter
given(mock).invocation { mockProperty }
    .then { mockPropertyData }

// Stub a property setter
given(mock).invocation { mockProperty = transformedData }
    .thenDoNothing()

Stubbing using Callable References

Callable references allows you to match arguments on something other than equality:

// Stub a `suspend` function
given(mock).suspendFunction(mock::fetchData)
    .whenInvokedWith(oneOf("mockative/mockative", "mockative/mockative-processor"))
    .then { data }

// Stub a blocking function
given(mock).function(mock::transformData)
    .whenInvokedWith(any())
    .then { transformedData }

// Stub a property getter
given(mock).getter(mock::mockProperty)
    .whenInvoked()
    .then { mockPropertyData }

// Stub a property setter
given(mock).setter(mock::mockProperty)
    .whenInvokedWith(matching { it.name == "foo" })
    .thenDoNothing()

// When the function being stubbed has overloads with a different number of arguments, a specific
// overload can be selected using one of the `fun[0-9]` functions.
given(mock).function(mock::transformData, fun0())
    .whenInvokedWith(any())
    .then { transformedData }

// When the function being stubbed has overloads with the same number of arguments, but different
// types, the type arguments must be specified using one of the `fun[0-9]` functions.
given(mock).function(mock::transformData, fun2<Data, String>())
    .whenInvokedWith(any(), any())
    .then { transformedData }

// Additionally, you can stub functions and properties by their name, but in this case you'll
// need to provide type information for the matchers.
given(mock).function("transformData")
    .whenInvokedWith(any<Data>())
    .then { transformedData }

Stubbing implementations

Both expressions using given and callable references using when[...] supports the same API for
stubbing the implementation, through the use of the then functions.

Function Description
then(block: (P1, P..., PN) -> R) Invokes the specified block. The arguments passed to the block are the arguments passed to the invocation.
thenInvoke(block: () -> R) Invokes the specified block.
thenReturn(value: R) Returns the specified value.
thenThrow(throwable: Throwable) Throws the specified exception.

When the return type of the function or property being stubbed is Unit, the following additional
then function is available:

Function Description
thenDoNothing() Returns Unit.

The untyped callable references using <T : Any> whenInvoking(T, String) and
<T : Any> whenSuspending(T, String) supports the following additional then function:

Function Description
then(block: (args: Array<Any?>) -> Any?) Invokes the specified block. The argument passed to the block is an array of arguments passed to the invocation.

Verification

Verification of invocations to mocks is supported through the verify(mock) API:

Verification using Expressions

// Expression (suspend function)
verify(mock).coroutine { fetchData("mockative/mockative") }
    .wasNotInvoked()

// Expression (blocking function)
verify(mock).invocation { transformData(data) }
    .wasInvoked(atLeast = 1.time)

// Expression (property getter)
verify(mock).invocation { mockProperty }
    .wasInvoked(atLeast = once, atMost = 6.times)

// Expression (property setter)
verify(mock).invocation { mockProperty = transformedData }
    .wasInvoked(exactly = 9.times)

Verification using Callable References

// Function Reference (suspend function)
verify(mock).coroutine(mock::fetchData)
    .with(eq("mockative/mockative"))
    .wasNotInvoked()

// Function Reference (blocking function)
verify(mock).function(mock::transformData)
    .with(any())
    .wasInvoked(atMost = 3.times)

// Getter
verify(mock).getter(mock::mockProperty)
    .wasInvoked(exactly = 4.times)

// Setter
verify(mock).setter(mock::mockProperty)
    .with(any())
    .wasInvoked(atLeast = 7.times)

Validation

// Verifies that all expectations were verified through a call to `verify(mock).wasInvoked()`.
verify(mock).hasNoUnverifiedExpectations()

// Verifies that the mock has no expectations that weren't invoked at least once.
verify(mock).hasNoUnmetExpectations()

GitHub

View Github