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 DependencyKeys (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…
MockspressoInstanceis like an immutableHashMapwhere the key is aDependencyKeyand the value is the dependency itself. (note: we rarely interact withMockspressoInstancein unit tests except for special edge-cases)MockspressoBuilderis 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.MockspressoPropertiesrepresents 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.Lazyis returned, granting a reference to the new object (note: referencing thisLazy.valuewill force theMockspressoInstanceto 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/mockkif the reference is not needed for the testMockspressoProperties.mock/mockkif the reference is needed for the testMockspressoProperties.spy/spykwrap 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
MockspressoBuilderfor plugins that add to the dependency graph but don’t need to return anything (builder plugins must always return the builder).MockspressoPropertiesfor plugins that need to add the dependency graph but also return a (lazy) reference.
Less Common
MockspressoInstancefor plugins that only need to pull from the dependency graph but do not need to add to it.Mockspressofor 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
RealObjectMakercreates real objects using some pre-defined rulesetFallbackObjectMakercreates objects (usually mocks) that aren’t explicitly registered in the graphDynamicObjectMakergets 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