Installation
See the Project Setup doc for current version & setup instructions.
Understanding the basics
Mockspresso creates real objects by scanning the constructor parameters and translating them into DependencyKey
s (which includes their type information and an optional Qualifier annotation). Tests “register” the dependencies they care about in a MockspressoBuilder
, and any dependencies that are missing from the registry are supplied by the FallbackObjectMaker
(usually with a mock).
When we interact with Mockspresso in a test it’s in one of 3 forms…
MockspressoInstance
is like an immutableHashMap
where the key is aDependencyKey
and the value is the dependency itself. (note: we rarely interact withMockspressoInstance
in unit tests except for special edge-cases)MockspressoBuilder
is what we use to put dependencies into aMockspressoInstance
. We can only put into a Builder, we cannot read from it until it’s been built.MockspressoProperties
represents a middle-state between Builder and Instance. The MockspressoInstance is ensured lazily under the hood, and the MockspressoBuilder remains mutable until that happens. When adding dependencies / declaring real objects onMockspressoProperties
, akotlin.Lazy
is returned, granting a reference to the new object (note: referencing thisLazy.value
will force theMockspressoInstance
to be ensured if it hasn’t already).
Declaring a real object
In unit tests we’re usually forced to declare all our dependencies first before we can declare our unit-under-test. With mockspresso, every unit test can declare it’s real object(s) up front using the MockspressoProperties.realInstance()
method.
class CoffeeMakerTest {
// See the Project Setup instructions re: setting up a custom
// MockspressoBuilder entry-point for your project
val mxo = MockspressoBuilder().build()
val coffeeMaker: CoffeeMaker by mxo.realInstance()
}
Assuming we’ve set up fallback mocking, we can actually start writing tests immediately and only add dependencies as we start requiring them. The CoffeeMaker will be supplied with all mocks until we start declaring dependencies.
Declaring dependencies
Dependencies for the real object can be declared using either
MockspressoBuilder.dependency
: if the reference is not needed for the testMockspressoProperties.dependency
: if the reference is needed for the testMockspressoProperties.fake<BIND, IMPL>
: if the reference needed by the test must be of a different type than the type of dependency bound in the real object
class CoffeeMakerTest {
val mxo = MockspressoBuilder()
.dependency<Timer> { FakeTimer() }
.build()
val coffeeMaker: CoffeeMaker by mxo.realInstance()
// TestFilter doesn't have any special methods we need
val filter: Filter by mxo.dependency<Filter> { TestFilter() }
// We need access to TestHeater.finishHeating()
val heater: TestHeater by mxo.fake<Heater, TestHeater> { TestHeater() }
}
Note: The filter
and heater
examples above should be rare in actual tests and would usually be substituted by plugins.
Declaring mock dependencies
The plugins-mockito
and plugins-mockk
modules include a few plugins to assist with mocking dependencies.
MockspressoBuilder.mock
/mockk
if the reference is not needed for the testMockspressoProperties.mock
/mockk
if the reference is needed for the testMockspressoProperties.spy
/spyk
wrap a real object with a spy
In each case the plugin signature mirrors what is supplied by the mocking framework. Example (using mockito):
class CoffeeMakerTest {
val mxo = MockspressoBuilder()
// mock an executor that we don't need a reference to but needs some setup
.mockock<Executor> {
on { execute(any()) } doAnswer { it.getArgument(0).run() }
}.build()
val coffeeMaker: CoffeeMaker by mxo.realInstance()
// let mockspresso create a real Filter, then wrap it with Mockito.spy
val filter: Filter by mxo.spy()
// mock a heater and include it as a dependency
val heater: Heater by mxo.mock()
@Test fun testHeater() {
coffeeMaker.brew()
verify(filter).clean()
verify(heater).heat(any())
}
}
Integration tests
When we create or declare any real object in mockspresso, that object becomes part of the underlying dependency graph. This means we can declare multiple real objects and run complex integration tests w/o ever worrying about constructor or dependency order. It also means our integrations tests won’t break by default just because dependencies have changed.
class CoffeeMakerTest {
val mxo = MockspressoBuilder()
.realInstance<Filter>() // a real Filter will be constructed
.realImplementation<Heater, HeaterImpl>() // a real HeaterImpl will be constructed
.build()
val coffeeMaker: CoffeeMaker by mxo.realInstance()
// a real FastGrinder will be constructed
val grinder: FastGrinder by mxo.realImplementation<Grinder, FastGrinder>()
}
Qualifier Annotations
All of the methods shown above actually include an optional qualifier Annotation?
as their first parameter. That is because every DI binding in mockspresso (aka DependencyKey
) is made up of both a TypeToken
and an optional qualifier Annotation?
. In the JVM, this means the annotation must, itself, be annotated with javax.inject.Qualifier
.
Example:
// real class
class MyRealClass @Inject constructor(
// Named is stock qualifier annotation included in jsr-330
@Named("IO") ioDispatcher: CoroutineContext
)
// test class
class MyRealClassTest {
val mxo = MockspressoBuilder()
.dependency<CoroutineContext>(createAnnotation<Named>("IO")) { EmptyCoroutineContext }
.build()
// will have EmptyCoroutineContext injected as dependency
val myRealClass: MyRealClass by mxo.realInstance()
}
Note: The createAnnotation()
method is part of kotlin-reflect
Developing Plugins
Mockspresso plugins are just kotlin extension functions targeting one of Mockspresso’s primary interfaces.
More Common
MockspressoBuilder
for plugins that add to the dependency graph but don’t need to return anything (builder plugins must always return the builder).MockspressoProperties
for plugins that need to add the dependency graph but also return a (lazy) reference.
Less Common
MockspressoInstance
for plugins that only need to pull from the dependency graph but do not need to add to it.Mockspresso
for test-framework support plugins that control the test lifecycle
Some plugin examples…
// MockspressoBuilder plugin to inject an EmptyCoroutineContext that is bound
// in DI as CoroutineContext
fun MockspressoBuilder.emptyCoroutineContext(qualifier: Annotation? = null): MockspressoBuilder =
dependency<CoroutineContext>(qualifier) { EmptyCoroutineContext }
// Usage:
val mxo = MockspressoBuilder()
.emptyCoroutineContext(createAnnotation<Named>("IO"))
.build()
// MockspressoProperties plugin to return a lazy of a TestCoroutineContext that is bound
// in DI as CoroutineContext
fun MockspressoProperties.testCoroutineContext(qualifier: Annotation? = null): Lazy<TestCoroutineContext> =
fake<CoroutineContext, TestCoroutineContext>(qualifier) { TestCoroutineContext() }
// Usage: (in the real tests we could drop the type)
val context: TestCoroutineContext by mxo.testCoroutineContext(createAnnotation<Named>("IO"))
There are also 3 types of plugins that can only be applied to MockspressoBuilder
RealObjectMaker
creates real objects using some pre-defined rulesetFallbackObjectMaker
creates objects (usually mocks) that aren’t explicitly registered in the graphDynamicObjectMaker
gets a chance to create any objects that aren’t explicitly registered in the graph
The DynamicObjectMaker
is worth calling out as it’s one of mockspresso’s more powerful concepts and powers the support of javax.inject.Provider
, dagger.Lazy
and @dagger.AssistedFactory
.
Included Plugins
See the Plugin Modules doc for a full list of the plugin modules mockspresso2 ships with