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…

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

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.

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

Less Common

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

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