deepsampler

DeepSampler is a stubbing framework for integrated compound tests.

License

License

GroupId

GroupId

de.ppi
ArtifactId

ArtifactId

deepsampler
Last Version

Last Version

1.0.0
Release Date

Release Date

Type

Type

pom.sha512
Description

Description

deepsampler
DeepSampler is a stubbing framework for integrated compound tests.
Project URL

Project URL

https://github.com/ppi-ag/deep-sampler
Source Code Management

Source Code Management

https://github.com/ppi-ag/deep-sampler

Download deepsampler

Dependencies

There are no dependencies for this project. It is a standalone project that does not depend on any other jars.

Project Modules

There are no modules declared in this project.

DeepSampler

Build & Test Coverage Bugs Code Smells Maintainability Rating Vulnerabilities

DeepSampler is a stubbing framework for compound tests. A compound is a net of objects. A compound can be isolated from an application but its content is integrated.

The API is able to stub methods anywhere deep inside of a compound of any size without the need to manually move a stubbed instance from a test case into the compound.

For tests with large test data (called Samples) DeepSampler can separate test logic from test data by loading and saving test data in JSON-files. The JSON-files can be recorded by activating the record-mode and simply running a test case. If a stub is in record-mode, it routes calls to the original methods and collects all data that flows through the stub. This collected data can then be saved to JSON-files.

For light-way tests with smaller Samples, where using separate JSON-files might be unnecessary, DeepSampler provides an API that can be used to define Samples conveniently inside test classes. The API also comes with means to completely redefine the behavior of stubbed methods.

Quickstart

The following tutorial demonstrates how to use DeepSampler with JUnit5 and Guice. You can download or clone the complete code from DeepSampler Examples.

Installation

We use Maven to build the example. So, as a first step, we add the following dependencies to our pom.xml:

<dependency>
   <groupId>de.ppi</groupId>
   <artifactId>deepsampler-junit5</artifactId>
   <version>1.0.4</version>
   <scope>test</scope>
</dependency>

<dependency>
   <groupId>de.ppi</groupId>
   <artifactId>deepsampler-provider-guice</artifactId>
   <version>1.0.4</version>
   <scope>test</scope>
</dependency>

The Testee

Let's say, we have a GreetingService that would create greeting messages for particular persons. We know only the ID of the person we want to greet, so the GreetingService needs to lookup the person somewhere (e.g. DB). This is done by a PersonService, which provides a method getName(personId). getName() in turn, would use a PersonDAO to load the Person from a Database.

Now we want to write a test for GreetingService and we want PersonService to be a part of the tested compound, so we cannot simply mock PersonService. Instead, we want to stub the PersonDAO in order to be independent of the database.

JUnit Test

We begin by setting up a JUnit Test. In general this will be an ordinary JUnit Test, but we use the DeepSamplerExtension to activate the DeepSampler environment:

@ExtendWith(DeepSamplerExtension.class)
class GreetingServiceTest {
   ...
}

๐Ÿ”Ž Note DeepSamplerExtension basically provides annotations as a means of convenience for most common use cases. However, DeepSampler can be used without this extension and even outside of JUnit Tests. (See Define a Sampler by API)

The actual stubbing is done by an aop-provider that uses - in this case - Guice aop. It is activated by passing the DeepSamplerModule to guice. In real life we would most likely have a Guice module for the testee, and we would want to combine the testee's module with DeepSamplerModule, however for the sake of simplicity we use DeepSamplerModule directly for now, when we tell Guice to inject members into our test class:

@BeforeEach
void injectWithGuice() {
   Guice.createInjector(new DeepSamplerModule()).injectMembers(this);
}

Define a stub

First we need a Sampler which serves as a model, that describes which methods should be stubbed and what the stub should do. The Sampler is an instance of the class, that we want to stub. We define the Sampler by adding a property with the type of the stubbed class and add the annotation PrepareSampler to it:

@ExtendWith(DeepSamplerExtension.class)
class GreetingServiceTest {

   @PrepareSampler
   private PersonDao personDaoSampler;
   ...
}

Inside of a concrete test method we would now use the Sampler to define a stub:

@Test
void greetingShouldBeGenerated() {
   Sample.of(personDaoSampler.loadPerson(1)).is(new Person("Sarek"));
   ...
}

This means, the method PersonDao::loadPerson will be stubbed. If loadPerson is called with the parameter 1 the stub will return the Sample new Person("Sarek"). Otherwise, the original method will be called. The stub applies to all instances of PersonDAO, no matter where the instances occur. It will be active for the lifetime of the test method, so each test method can declare its own stubs.

That's it, we can now call the testee and check the results as usual:

@Test
void greetingShouldBeGenerated() {
   Sample.of(personDaoSampler.loadPerson(1)).is(new Person("Sarek"));
                                                    ๐Ÿ‘‡
   assertEquals("Hello Sarek!", greetingService.createGreeting(1));
}

๐Ÿ”Ž Note Just to see that the stub is actually doing something, we could clear all stubs and repeat the assertion. Now the original method is used again, and we get another return value:

void greetingShouldBeGenerated() {
   Sample.of(personDaoSampler.loadPerson(1)).is(new Person("Sarek"));

   assertEquals("Hello Sarek!", greetingService.createGreeting(1));
             ๐Ÿ‘‡
   Sampler.clear();
                          ๐Ÿ‘‡
   assertEquals("Hello Geordi La Forge!", greetingService.createGreeting(1));
}

Define a Sampler by API

If you want to use DeepSampler outside of JUnit or don't want to use the annotations, it is possible to create a Sampler using the API:

PersonDao personDaoSampler = Sampler.prepare(PersonDao.class);

SamplerFixtures

Usually it will not be enough to write just one test for a particular compound like the GreetingService, so it would be tedious to repeat the definition of stubs in each test case. To ease that, SamplerFixtures can be used to define reusable stubs. You can also think of a SampleFixture as a definition of a test compound, because the stubs would isolate the compound from the environment in order to make it testable:

public class GreetingServiceCompound implements SamplerFixture {
   @PrepareSampler
   private PersonDao personDaoSampler;

   @Override
   public void defineSamplers() {
      Sample.of(personDaoSampler.loadPerson(Matchers.anyInt()));
   }
}

Once we have such a SamplerFixture, we can bind it to a test case using the annotation @UseSamplerFixture like so:

@Test
@UseSamplerFixture(GreetingServiceCompound.class)
void recordSamplesToJson() {
   ...
}

The stubs, that are defined by the SamplerFixture, are now active for the time the method runs.

You might have noticed that the stub doesn't define a concrete Sample anymore. We have done this, because we rather want to read the Samples from a JSON-file now. How this can be achieved, is explained by the next chapter.

Persistent Samples

For the sake of understandability, the tested compound in this example is fairly simple, however DeepSampler was especially designed for complex compound tests with quite a lot of stubs which also might return extensive Samples. In these cases we would not want to have big sets of test data (Samples) in JUnit Test classes, we would rather separate test data from test logic. This can be done by saving and loading Samples from JSON-files.

Record a JSON-Sample

In order to save Samples in a JSON-file, we simply add the annotation @SaveSamples to our method and call the method, which we want to test. The methods that should be stubbed are defined by the SamplerFixture that we have introduced in the preceding chapter.

@Test
@SaveSamples
@UseSamplerFixture(GreetingServiceCompound.class)
void recordSamplesToJson() {
   greetingService.createGreeting(1);
}

When we run this test, the stubs will call the original methods and additionally record the parameters and return values for each call. The recorded data is then saved to a JSON-file.

By default @SaveSamples saves the JSON-file in a folder corresponding to the package of the current test case. The filename is created using the class name, and the method name of the package. In this case we would get a file named ./de/ppi/deepsampler/examples/helloworld/GreetingServiceTest_recordSamplesToJson.json.

Load a JSON-Sample

Finally, we can use a SamplerFixture, and a JSON-file to build a test case. A JSON-file can be loaded using the annotation @LoadSamples:

@Test
@LoadSamples
@UseSamplerFixture(GreetingServiceCompound.class)
void loadSamplesFromJson() {
   assertEquals("Hello Sarek!", greetingService.createGreeting(1));
}

By default LoadSamples searches for the JSON-file on the classpath. It expects the JSON-file in the same package where the current test case is located. The file name is created using the class name, and the method name of the package. In this case DeepSampler would try to load a file named ./de/ppi/deepsampler/examples/helloworld/GreetingServiceTest_loadSamplesFromJson.json.

Scopes

DeepSampler is by default Thread-scoped. So Samples, that have been defined in one Thread, are available only in this particular Thread.

But you can change the Scope using SampleRepository::setScope. DeepSampler comes with two predefined Scopes:

  • ThreadScope: Samples are Thread-exclusive, this is the default.
  • SingeltonScope: The same Samples are available across the entire VM and all Threads share the same Samples.

You can also define your own custom Scope by implementing the interface de.ppi.deepsampler.core.model.Scope.

๐Ÿ”Ž Note the Scope must be changed before the first Samples have been defined.

The following line would make all Samples available across all Threads:

    SampleRepository.setScope(new SingletonScope());

License

DeepSampler is made available under the terms of the MIT License (see LICENSE.md).

Copyright 2020 PPI AG (Hamburg, Germany)

de.ppi

PPI Informationstechnologie

http://www.ppi.de

Versions

Version
1.0.0