Skip to content

mockative/mockative

main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
March 29, 2023 07:23
March 28, 2023 15:10
March 28, 2023 15:10
December 13, 2021 12:59
November 12, 2021 21:46
November 12, 2021 22:11
March 29, 2023 13:57
March 28, 2023 15:11
November 1, 2021 13:32
November 1, 2021 13:32

Mockative

Maven Central

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

Installation for Multiplatform projects

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.4.0")
            }
        }
    }
}

dependencies {
    configurations
        .filter { it.name.startsWith("ksp") && it.name.contains("Test") }
        .forEach {
            add(it.name, "io.mockative:mockative-processor:1.4.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(classOf<GitHubAPI>())
}

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 and callable references 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 function(String) and suspendFunction(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.

Implicit stubbing of functions returning Unit

Mockative comes with an opt-in feature for stubbing functions returning Unit by default, based on the idea that such functions are typically used for verification rather than stubbing, and are trivially stubbed automatically.

You can opt-in to this behaviour on the project level through your build.gradle.kts file:

build.gradle.kts

ksp {
    arg("mockative.stubsUnitByDefault", "true")
}

Alternatively, you can opt-in (or opt-out if you've opted in on the project level), using the configure(mock, block) function either inline:

@Mock val api = configure(mock(classOf<GitHubAPI>())) { stubsUnitByDefault = true }

Or as needed:

@Mock val api = mock(classOf<GitHubAPI>())

@Test
fun test() {
    configure(api) { stubsUnitByDefault = true }
}

Generic Types

When mocking a generic type, use the <T> classOf(): KClass<T> function to retain the type arguments when passed to the <T> mock(KClass<T>) function.

class GenericTypeTest {
    @Mock
    val list = mock(classOf<List<String>>())
}

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).suspendFunction(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()