Getting Started with Mockspresso

Since Mockspresso is agnostic to your DI and mocking framework of choice, we must start with an empty Mockspresso.Builder() an teach it how to create mocks and real objects. We usually do this using kotlin extension functions…

Kotlin Example

BuildMockspresso.with()
    .injectBySimpleConfig() // extension functions on Mockspresso.Builder
    .mockByMockito()

We also provide plugins for java callers that can be applied using Mockspresso.Builder.plugin(MockspressoPlugin). As a pattern, Java support plugins are provided by identically named (static) methods that return MockspressoPlugins (and are not visible to kotlin code).

Java Example

BuildMockspresso.with()
    .plugin(MockspressoBasicPluginsJavaSupport.injectBySimpleConfig())
    .plugin(MockspressoBasicPluginsJavaSupport.mockByMockito())

See INCLUDED_PLUGINS for a list of included injectors and mockers.

Project Entry-Point

Since most of the tests in a single project are likely to use the same mocker & injector, it’s a best-practice to set up a single entry-point for all of a project’s mockspresso tests…

object BuildMockspresso {
  @JvmStatic
  fun withDefaults(): Builder = com.episode6.hackit.mockspresso.BuildMockspresso.with()
      .injectBySimpleConfig()
      .mockByMockito()
}

JUnit Rule

The simplest way to set up a mockspresso test is by applying a JUnit Rule via Mockspresso.Builder.buildRule(). Applying the rule will automatically trigger annotation processing on your test for mock generation, dependency import and real object creation.

// kotlin with mockito/mockito-kotlin
class CoffeeMakerHeaterTest {

    // Note: the `@get:` syntax is required of all junit rules in kotlin tests
    @get:Rule val mockspresso = BuildMockspresso.withDefaults().buildRule()

    // declare only the mocks we need for our test
    @Dependency val heater: Heater = mock()

    // real object will be created for us
    @RealObject lateinit var coffeeMaker: CoffeeMaker

    @Test fun testHeaterIsUser() {
        val coffee = coffeeMaker.brew()

        verify(heater).heat(any())
    }
}

Note: there is also a build() method available, but most unit tests will find buildRule() more convenient. See Mockspresso on-the-fly for details.

Annotations

When using the mockspresso junit rule, mockspresso will perform some reflection-based annotation processing on your test class.

@Dependency: Marks a final field or val that should be part of the mockspresso dependency graph.

// supply this TestHeater wherever a Heater is injected
@Dependency val heater: Heater = TestHeater()

// supply a mock using mockito-kotlin syntax
@Dependency val waterFilter: WaterFilter = mock()

// (rx example) bind this TestScheduler as a Scheduler in the graph but
// retain access to all TestScheduler methods in these tests.
@Dependency(bindAs = Scheduler::class) val testScheduler = TestScheduler()

// Apply a qualifier annotation to the field if the object-under-test uses one to inject the dependency.
@Dependency @field:Named("brew_temp") val brewTemperature: Int = 100

@Mock: EasyMock & Mockito’s @Mock annotations are automatically recognized as dependencies when the appropriate mocker is used. The mockspresso rule will also take care of initializing the mocks, so there’s no need to apply the EasyMock or Mockito rules or init annotations.

// Automatically create a mock here and include it in our mockspresso graph
@Mock lateinit var waterFilter: WaterFilter

@RealObject: This is where the magic happens. Lateinit variables annotated with @RealObject will be created by mockspresso and injected with the dependencies included in the graph. Any dependencies required by the object that are not explicitly included in the mockspresso graph, will be automatically mocked. The resulting realObject also gets included in the graph, and can be a dependency of another @RealObject, enabling complex integration tests with simplified setups and zero boilerplate constructor calls.

// create a real SimpleCoffeeMaker for us to test
@RealObject lateinit var SimpleCoffeeMaker: SimpleCoffeeMaker

// create a real SimpleCoffeeMaker, but cast it as a generic CoffeeMaker
// both in this local variable an in the mockspresso graph
@RealObject(implementation = SimpleCoffeeMaker::class)
lateinit var coffeeMaker: CoffeeMaker

// apply a qualifier annotation to the object's binding in
// the mockspresso dependency graph
@RealObject @field:Named("simple")
lateinit var coffeeMaker: CoffeeMaker

Builder methods

While mockspresso’s annotation processing is usually the simplest way to add dependencies to the graph, we can also perform all the same operations using the methods (and extension methods) on Mockspresso.Builder. This can be useful if either a) we don’t need/want a strong reference to a dependency in our test, or b) we want to make some common dependencies shareable by all tests.

adding dependencies

The dependency methods of Mockspresso.Builder allow us to add arbitrary objects to our dependency graph.

For example, if our class-under-test required an injected Rx Scheduler, we could bind the trampoline scheduler w/o retaining our own reference to it.

declaring real objects

When building complex integration tests with multiple real objects we rarely need to hold references to all of them directly once we’ve removed the need to call constructors. The Mockspresso.Builder allows us to declare that a specific dependency class/type should be created instead of mocked, and have its dependencies injected. If the dependency is bound in DI as an interface (or open class) we can specify the implementation we want as well.

For example, if we wanted to ensure our WaterFilter was a real object instead of a mock, we could apply the following…

If WaterFilter is an interface, we could apply the following…

Reference the Mockspresso.Builder java docs for a complete list of builder methods.

Special Object Makers

Mockspresso special object makers allow us to customize the creation/mocking of objects based on their type and qualifier. They are also able to pull from mockspresso’s dependency graph in order to map from one type/dependency to another. For example, the built-in automaticProviders() plugin leverages a special object maker to generate javax.inject.Provider<T>s that automatically map to a dependency of T.

Example…

class ClassUnderTest @Inject constructor(
  private val stringProvider: javax.inject.Provider<String>
) {
  fun string() = stringProvider.get()
}

class TestClass {
  @get:Rule val mockspresso = BuildMockspresso.withDefaults()
      .automaticProviders() // this isn't necessary if we use .injectByJavaxConfig()
      .buildRule()

  @RealObject lateinit var objUnderTest: ClassUnderTest
  @Dependency val str = "hello"

  @Test fun verifyString() {
    assertThat(objUnderTest.string()).isEqualTo(str)
  }
}

Custom special object makers can be added via Mockspresso.Builder.specialObjectMaker()

Test Resources

We’ve shown how, with the junit rule, mockspresso can automatically perform annotation processing on your test class. If we want to perform the same annotation processing on arbitrary objects other than the test class, we can do that using the Mockspresso.Builder.testResources(Object) method. In addition to processing annotations for @Dependency @RealObject and @Mock, mockspresso will also find and execute methods annotated with junit’s @Before and @After annotations (to avoid this, use Mockspresso.Build.testResourcesWithoutLifecycle() instead).

Example…

class SharedTestResources {
  @Mock lateinit var dep: ComplexDependency

  @Before fun setup() {
    whenever(dep.doThing).thenAnswer { /* complex return */ }
  }
}

class ActualTest {
  @get:Rule val mockspresso = BuildMockspresso.withDefaults()
      .testResources(SharedTestResources())
      .buildRule()

  @RealObject lateinit val complexDepWrapper: complexDepWrapper

  @Test fun testComplexDep() { /* etc... */ }
}

Mockspresso on-the-fly

While a junit rule is the most common way to build mockspresso, instances can also be built and built-upon on-the-fly. When we use the Mockspresso.Builder.build() method instead of buildRule, our Mockspresso instance can be ready for use immediately, but we won’t trigger the same automatic annotation processing (we can still leverage the Test Resources feature to process annotations on arbitrary objects).

For example, we could replace junit rule example with an on-the-fly instance…

 class CoffeeMakerHeaterTest {
-    @get:Rule val mockspresso = BuildMockspresso.withDefaults().buildRule()

     @Mock lateinit var heater: Heater
     @RealObject lateinit var coffeeMaker: CoffeeMaker
 
+    @Before fun setup() {
+      val mockspresso = BuildMockspresso.withDefaults()
+          .testResourcesWithoutLifecycle(this)
+          .build()
+    }   

    @Test fun testHeaterIsUser() {
        val coffee = coffeeMaker.brew()

        verify(heater).heat(any())
    }
 }

Once built, we can also create real objects and get dependencies programmatically as needed from the Mockspresso instance.

 class CoffeeMakerHeaterTest {
-    @Mock
     lateinit var heater: Heater
-    @RealObject
     lateinit var coffeeMaker: CoffeeMaker
 
    @Before fun setup() {
      val mockspresso = BuildMockspresso.withDefaults()
          .testResourcesWithoutLifecycle(this)
          .build()
+     coffeeMaker = mockspresso.createNew()
+     heater = mockspresso.getDependencyOf()
    } 

    @Test fun testHeaterIsUser() {
        val coffee = coffeeMaker.brew()

        verify(heater).heat(any())
    }
 }

While we can’t make changes to a Mockspresso instance once it’s been built, we can buildUpon() any mockspresso instance to create a new subgraph. We can override any dependencies in our sub-graph and create new realObjects with the updated deps.

For example…

@Test fun testWithRealHeater() {
  val coffeeMaker = mockspresso.buildUpon() // buildUpon an existing graph
      .realImplOf<Heater, WorkingHeater>() // override the Heater dependency
      .build() // build the sub-graph
      .createNew<CoffeeMaker>() // create new coffeeMaker from subgraph

  val coffee = coffeeMaker.brew()

  assertThat(coffee).isHot()
}

The buildUpon method works both ways between on-the-fly mockspresso instances and rules, but only 1 rule can be generated in the chain.