Skip to content

A library for stubbing/faking data objects in unit tests for example

License

Notifications You must be signed in to change notification settings

Double-O-Seven/stubr

Repository files navigation

Actions Status Release Version Codacy Badge Codacy Badge

stubr

Stubr is an implementation of the "Object Mother" pattern and is a small, extensible library for creating stubs or fixtures for test data in unit tests.

Basics

The basic concept is very simple: You build a stubber, using different stubbing strategies for different classes and types and then you request a specific instance for your desired type.

For example:

public class Fubar {

        public final String foo;
        public final int bar;
        public final int fubar;
        
        public Foo(String foo, int bar, int fubar) {
                this.foo = foo;
                this.bar = bar;
                this.fubar = fubar;
        }
}


Stubber stubber = Stubber.builder()
        .stubWith(StubbingStrategies.constructor()) // Instantiate an object using a constructor
        .stubWith(StubbingStrategies.constantValue("Foo")) // Provide "Foo" when a String is required
        .stubWith(StubbingStrategies.suppliedValue(int.class, (int sequenceNumber) -> sequenceNumber)) // Provide a sequence value when an int is required
        .build();
String stringValue = stubber.stub(String.class); // Foo
int intValue1 = stubber.stub(int.class); // 0
int intValue2 = stubber.stub(int.class); // 1
Fubar fubar = stubber.stub(Fubar.class); // Fubar(foo = "Foo", bar = 2, fubar = 3)

But why?

Stubr is an implementation of the "Object Mother" pattern and is designed to complement existing mocking framework like Mockito or MockK. While those frameworks are often used to mock certain behaviour, Stubr was created to provide valid, commonly unmocked, instances of data objects.

Sometimes in your test case, you will access various values of a data object. The accessed values may potentially be non-null. This will leave you with the following options:

  • Manually set up the whole data object
  • You mock the data object and define the return values

In both cases you end up with a lot of boiler plate code, especially when you need to set up or mock data whose content is not actually relevant to the test case.

A possible solution for the problem described above is Stubr. Stubr can instantiate valid data objects for you, using suitable default or custom stubbing strategies.

When using immutable data classes Stubr works especially well with Kotlin data classes which provide a copy() method. It will instantiate a valid instance from which you can derive a copy where you set the relevant properties yourself. The same applies when using @Builder(toBuilder = true) annotated Lombok data classes for example.

In the samples project you will also find an example where the test subject of a unit test is automatically instantiated with all dependencies being mocked with Mockito.

Concepts

Stubbing strategies

A Stubber is composed using different StubbingStrategys. A StubbingStrategy may provide stub values for only a specific type or it may provide a generic solution to provide stubs for suitable types.

It consist of two basic methods, that depend on each other

  • boolean accepts(StubbingContext context, Type type)
  • Object stub(StubbingContext context, Type type)

accept determines whether a StubbingStrategy is applicable for the given type and context. The StubbingContext consists of the calling Stubber as well as the StubbingSite which describes where the stub value was requested.

stub implements the provision of the stub value, which might be a fixed value, might depend on the type or context or might even be a random value.

Any implementation of stub may assume that the given context and type are accepted, so double-checking the input is not required.

A custom implementation may be used as follows:

StubbingStrategy myAmazingStrategy = new MyAmazingStrategy();
Stubber stubber = Stubber.builder()
        .stubWith(myAmazingStrategy)
        .build();

Multiple strategies might accept the same types and therefore conflict each other. The behaviour in this case is that the Stubber will prioritize the StubbingStrategy that was added the last during the build of the stubber Stubber. The rule is therefore that the most generic strategies should be added first, while the most specific strategies should be added last.

For example:

Stubber stubber = Stubber.builder()
        .stubWith(veryGenericStrategy) // Accepts int, float and Strings for example
        .stubWith(verySpecificStrategy) // Accepts only int
        .build();

In the above example, the veryGenericStrategy accepts multiple types, while the verySpecificStrategy only accepts ints. The built Stubber will therefore use verySpecificStrategy to provide stub values for ints and use veryGenericStrategy for other types.

Various StubbingStrategy implementations can be found in ch.leadrian.stubr.core.strategy.StubbingStrategies.

Stubbing sites

StubbingStrategys can use the calling Stubber themselves to instantiate stubs. An simple example for that is a stubbing strategy that instantiates a stub using a suitable constructor. This constructor however, might itself need parameter values which can be provided by the Stubber.

A StubbingSite describes where exactly and under what circumstances a StubbingStrategy requests additional stub values from the calling Stubber. This information can then be used to provide different values of the same type for different StubbingSites. One might for example configure the Stubber to provide a null value when a method parameter is annotated with @Nullable and non-null value otherwise.

Various StubbingSite implementations can be found in ch.leadrian.stubr.core.site.StubbingSites.

Matchers

A StubbingStrategy has one additional default method:

StubbingStrategy when(Matcher<? super Type> typeMatcher)

This method returns a new StubbingStrategy that uses the receiver (this) to stub values, but in addition only accepts a given context and type if the given Matcher matches both the context and type.

It is therefore possible to conditionally apply StubbingStrategys, for example depending on the StubbingSite or the type or both.

A Matcher<T> is a generic functional interface that requires the following method to be implemented:

boolean matches(StubbingContext context, T value)

Since Matchers are generic, a matcher may be combined using logical and, or and not operations, and in addition matcher might delegate to other matches in order to match for example directly the StubbingSite.

Various Matcher implementations can be found in ch.leadrian.stubr.core.matcher.Matchers.

Example

The following example discribes a situation when a constructor is used to stub a value and one of its parameters is annotated with @One. In this case, the integer stub value passed to the constructor would always be 1.

import static ch.leadrian.stubr.core.strategy.StubbingStrategies.*;
import static ch.leadrian.stubr.core.matcher.Matchers.*;

Stubber.builder()
    // ... more configuration
    .stubWith(constantValue(1).when(site(constructor(annotatedWith(One.class))))
    .build()

Selector

A Selector<T> is a generic functional interface that selects zero or one values from a given list of values:

Optional<T> select(StubbingContext context, List<? extends T> values)

In core implementations of StubbingStrategys Selectors are used to select an enum constant, a constructor or a factory method.

Various Selector implementations can be found in ch.leadrian.stubr.core.selector.Selectors.

Download

For Maven:

<dependency>
  <groupId>ch.leadrian.stubr</groupId>
  <artifactId>stubr-core</artifactId>
  <version>2.0.1</version>
</dependency>

For Gradle (Groovy DSL):

implementation 'ch.leadrian.stubr:stubr-core:2.0.1'

For Gradle (Kotlin DSL):

implementation("ch.leadrian.stubr:stubr-core:2.0.1")

3rd party support

JUnit 5

Stubr also includes an extension for JUnit Jupiter that allows you to get stub values through parameter injection. The only requirement is to extend your test with the extension ch.leadrian.stubr.junit.Stubr and to annotated the desired method parameter with ch.leadrian.stubr.junit.annotation.Stub.

In addition, the tests might be configured using ch.leadrian.stubr.junit.Include, ch.leadrian.stubr.junit.StubWith and ch.leadrian.stubr.junit.StubberBaseline.

For example:

@ExtendWith(Stubr.class)
@StubWith(MyAmazingStubberProvider.class)
class MyAmazingTest {

    @Test
    void testSomething(@Stub String value) {
        // test something
    }

}

The JUnit 5 extension can be downloaded here:

For Maven:

<dependency>
  <groupId>ch.leadrian.stubr</groupId>
  <artifactId>stubr-junit</artifactId>
  <version>2.0.1</version>
</dependency>

For Gradle (Groovy DSL):

implementation 'ch.leadrian.stubr:stubr-junit:2.0.1'

For Gradle (Kotlin DSL):

implementation("ch.leadrian.stubr:stubr-junit:2.0.1")

Mockito

Stubr also includes a Mockito module that provides StubbingStrategy implementations that provide stubs using Mockito mocks. All non-void method call on such a mock will return a stub value provided by the Stubber that was used to mock the stub.

Concrete instances can be accessed through ch.leadrian.stubr.mockito.MockitoStubbingStrategies.

A usage example:

Stubber stubber = Stubber.builder()
    .stubWith(StubbingStrategies.constantValue("stubbed"))
    .stubWith(MockitoStubbingStrategies.mock())
    .stubWith(MockitoStubbingStrategies.mock(MyMockedObject.class, mock -> Mockito.when(mock.doSomething()).thenReturn("done")))
    .build();
Foo someOtherMock = stubber.stub(Foo.class); // Stubbed using the generic mock stubbing strategy
MyMockedObject obj = stubber.stub(MyMockedObject.class); // Stubbed using the specific mock stubbing strategy
String result = obj.doSomething(); // result = "done"

The Mockito extension can be downloaded here:

For Maven:

<dependency>
  <groupId>ch.leadrian.stubr</groupId>
  <artifactId>stubr-mockito</artifactId>
  <version>2.0.1</version>
</dependency>

For Gradle (Groovy DSL):

implementation 'ch.leadrian.stubr:stubr-mockito:2.0.1'

For Gradle (Kotlin DSL):

implementation("ch.leadrian.stubr:stubr-mockito:2.0.1")

Kotlin

Stubr also includes a Kotlin extension to simplify method calls and to stub Kotlin object classes.

object MyObject {

    fun doSomething() {
    }

}

data class MyData(val foo: String) {

    constructor() : this("default-value")

}

var stubber = Stubber.builder()
    .stubWith(StubbingStrategies.constantValue("stub-value"))
    .stubWith(KotlinStubbingStrategies.objectInstance())
    .stubWith(KotlinStubbingStrategies.primaryConstructor())
    .build()
val myObject: MyObject = stubber.stub()
val myData: MyData = stubber.stub() // MyData(foo = "stub-value"), instantiated with primary constructor

The Kotlin extension can be downloaded here:

For Maven:

<dependency>
  <groupId>ch.leadrian.stubr</groupId>
  <artifactId>stubr-kotlin</artifactId>
  <version>2.0.1</version>
</dependency>

For Gradle (Groovy DSL):

implementation 'ch.leadrian.stubr:stubr-kotlin:2.0.1'

For Gradle (Kotlin DSL):

implementation("ch.leadrian.stubr:stubr-kotlin:2.0.1")

MockK

Stubr also includes a MockK module that provides StubbingStrategy implementations that provide stubs using MockK mocks. The provided stubs are by default relaxed MockK mocks.

Concrete instances can be accessed through ch.leadrian.stubr.mockk.MockKStubbingStrategies.

A usage example:

val stubber = Stubber.builder()
    .stubWith(MockKStubbingStrategies.mockkAny())
    .stubWith(MockKStubbingStrategies.mockk<Foo> {
        every { getSomeValue() } returns "Hello there!"
    })
    .build();
val foo = stubber.stub<Foo>() // Stubbed with the mockk() stubbing strategy
val result = foo.getSomeValue() // result = "Hello there!"
val bar = stubber.stub<Bar>() // Stubbed with the mockkAny() stubbing strategy
val otherResult = bar.getSomeOtherValue() // returns default value given by MockK

The Mockito extension can be downloaded here:

For Maven:

<dependency>
  <groupId>ch.leadrian.stubr</groupId>
  <artifactId>stubr-mockk</artifactId>
  <version>2.0.1</version>
</dependency>

For Gradle (Groovy DSL):

implementation 'ch.leadrian.stubr:stubr-mockk:2.0.1'

For Gradle (Kotlin DSL):

implementation("ch.leadrian.stubr:stubr-mockk:2.0.1")