Java EcoSystem Integration

Home

Appendices


© 2019-2020 rxmicro.io. Free use of this software is granted under the terms of the Apache License 2.0.

Copies of this entity may be made for Your own use and for distribution to others, provided that You do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.

If You find errors or omissions in this entity, please don’t hesitate to submit an issue or open a pull request with a fix.

To help You write tests efficiently, the RxMicro framework provides the following modules:

Using these modules, the developer can create the following types of tests:

1. Preparatory Steps

Before writing tests, using the RxMicro framework, the following steps must be taken:

  1. Define the versions of used libraries.

  2. Add the required dependencies to the pom.xml.

  3. Configure the maven-compiler-plugin.

  4. Configure the maven-surefire-plugin.

1.1. Definition the Versions of the Used Libraries:

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <rxmicro.version>0.10-SNAPSHOT</rxmicro.version> (1)

    <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version> (2)
    <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>(3)
</properties>

1.2. Adding the Required Dependencies:

Before using the RxMicro modules for testing, You need to add the following dependencies to the project:

<dependencies>
    <dependency>
        <groupId>io.rxmicro</groupId>
        <artifactId>rxmicro-test-junit</artifactId> (1)
        <version>${rxmicro.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.rxmicro</groupId>
        <artifactId>rxmicro-test-mockito-junit</artifactId> (2)
        <version>${rxmicro.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.rxmicro</groupId>
        <artifactId>rxmicro-rest-client-exchange-json</artifactId> (3)
        <version>${rxmicro.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>
1 rxmicro-test-junit - a unit testing library based on the JUnit 5 framework.
2 rxmicro-test-mockito-junit - a unit testing library based on the JUnit 5 framework with integration of the Mockito framework.
3 rxmicro-rest-client-exchange-json - a library for converting Java models to JSON format and vice versa on the HTTP client side.

The rxmicro-rest-client-exchange-json libraries are required only during writing the REST-based microservice tests and integration tests. These dependencies can be omitted during writing the component unit tests.

The rxmicro-test-mockito-junit library depends on the rxmicro-test-junit, so only one of them needs to be added:

  1. If only the JUnit 5 framework is required, use the rxmicro-test-junit library.

  2. If You still need to create mocks, then use the rxmicro-test-mockito-junit library.

1.3. Configuring the maven-compiler-plugin:

To solve problems with the Java module system when writing the tests, it is necessary to add the additional execution to the maven-compiler-plugin configuration:

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${maven-compiler-plugin.version}</version> (1)
    <configuration>
        <release>11</release>
        <annotationProcessorPaths>
            <annotationProcessorPath>
                <groupId>io.rxmicro</groupId>
                <artifactId>rxmicro-annotation-processor</artifactId>
                <version>${rxmicro.version}</version>
            </annotationProcessorPath>
        </annotationProcessorPaths>
    </configuration>
    <executions>
        <execution>
            <id>source-compile</id>
            <goals>
                <goal>compile</goal>
            </goals>
            <configuration>
                <annotationProcessors>
                    <annotationProcessor>
                        io.rxmicro.annotation.processor.RxMicroAnnotationProcessor
                    </annotationProcessor>
                </annotationProcessors>
                <generatedSourcesDirectory>
                    ${project.build.directory}/generated-sources/
                </generatedSourcesDirectory>
            </configuration>
        </execution>
        <execution>
            <id>test-compile</id> (2)
            <goals>
                <goal>testCompile</goal>
            </goals>
            <configuration>
                <annotationProcessors>
                    <annotationProcessor>
                        io.rxmicro.annotation.processor.RxMicroTestsAnnotationProcessor (3)
                    </annotationProcessor>
                </annotationProcessors>
                <generatedTestSourcesDirectory>
                    ${project.build.directory}/generated-test-sources/ (4)
                </generatedTestSourcesDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>
1 The plugin version defined in the properties section.
2 The separate configuration is required for the tests, so a new execution must be added.
3 The annotation processor class that handles the test configuration.
4 Location of Java classes generated by the RxMicro Test Annotation Processor.

The io.rxmicro.annotation.processor.RxMicroTestsAnnotationProcessor generates additional classes required only during testing.

Therefore, You must always specify a separate folder for the generated classes!

1.4. Configuring the maven-surefire-plugin:

For a successful tests launch while building a project with maven it is necessary to update maven-surefire-plugin:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>${maven-surefire-plugin.version}</version> (1)
    <configuration>
        <properties>
            <!-- https://junit.org/junit5/docs/5.5.1/api/org/junit/jupiter/api/Timeout.html -->
            <configurationParameters>
                junit.jupiter.execution.timeout.default = 60                (2)
                junit.jupiter.execution.timeout.mode = disabled_on_debug    (3)
                junit.jupiter.execution.parallel.enabled = false            (4)
            </configurationParameters>
        </properties>
    </configuration>
</plugin>
1 Last stable version of maven-surefire-plugin.
(The plugin version must be 2.22.1 or higher, otherwise maven will ignore the tests!.)
2 In case of an error in the code which uses reactive programming, an infinite function execution may occur. In order to detect such cases, it is necessary to set a global timeout for all methods in the tests. (By default, timeout is set in seconds. More detailed information on timeouts configuration is available in official JUnit5 documentation.)
3 While debugging, timeouts can be turned off.
4 This property is useful for the tests debugging from IDE or maven.
(By setting this property the speed of test performance will decrease, so use this property for debugging only!)

At the end of each subsection describing any the RxMicro framework feature, there is a link to the project, the source codes of which were used in this subsection.

Use these links to access a working example demonstrating the RxMicro framework feature described above!

2. RxMicro Test Annotations

The RxMicro framework supports additional RxMicro Test annotations, that extend the features of the JUnit 5, Mockito and DbUnit test frameworks and add additional features required for efficient writing of all supported test types.

The RxMicro framework supports two types of annotations:

  • The RxMicro Annotations used by the RxMicro Annotation Processor during the compiling and therefore available only during the compilation process.

  • The RxMicro Test Annotations used by the test framework during the test run and therefore available using the reflection mechanism.

These types of annotations do not complement each other! Each of these types is designed to perform its tasks.

So when writing tests, be careful not to use the RxMicro Annotations as they are not available for the test framework!

Table 1. Supported RxMicro Test Annotations
Annotation Description

@Alternative

Declares the test class field as an alternative.

The RxMicro framework supports alternatives only for REST-based microservice tests and component unit tests.

@WithConfig

Declares the static field of the test class as a configuration which must be registered in the configuration manager before starting the test.

This annotation allows declaring a configuration using Java classes. (The configuration defined in this way is only available while the test is running.)

The source code of the project that uses the @WithConfig annotation, is available at the following link:

testing-microservice-with-config

The RxMicro framework supports test configuration only for REST-based microservice tests and component unit tests.

@SetConfigValue

Allows overriding the default value for any configuration available only for the test environment.

This annotation allows declaring a configuration using annotations. (The configuration defined in this way is only available while the test is running. It means that this annotation is analogous to the DefaultConfigValue annotation!)

The RxMicro framework supports test configuration only for REST-based microservice tests and component unit tests.

@BlockingHttpClientSettings

Allows to configure the following component: BlockingHttpClient, in order to execute HTTP requests in tests.

(This annotation applies only to the BlockingHttpClient type fields.)

The RxMicro framework supports the BlockingHttpClient component only for REST-based microservice tests and REST-based microservice integration tests.

@RxMicroRestBasedMicroServiceTest

Declares the test class as a REST-based microservice test.

@RxMicroComponentTest

Declares the test class as a component unit test.

@RxMicroIntegrationTest

Declares the test class as a REST-based microservice integration test.

@DbUnitTest

Declares the test class as a DBUnit integration test.

@BeforeThisTest

It is used to specify the method to be invoked by the RxMicro framework before running the test method.

The RxMicro framework supports the @BeforeThisTest annotation only for REST-based microservice tests and component unit tests.

@BeforeIterationMethodSource

It is used to specify the methods to be invoked by the RxMicro framework before performing each iteration of the parameterized test.

The RxMicro framework supports the @BeforeIterationMethodSource annotation only for REST-based microservice tests and component unit tests.

@InitMocks

Informs the test framework about the need to create mocks and inject them into the test class fields, annotated by the @Mock annotation.

(Using the @InitMocks annotation is preferable to the analogous @ExtendWith(MockitoExtension.class) construction.)

@InitialDataSet

Provides the init state of tested database before execution of the test method.

@ExpectedDataSet

Provides the expected state of tested database after execution of the test method.

If expected state does not match to the actual database state the java.lang.AssertionError will be thrown.

@RollbackChanges

Declares the transactional test.

The transaction test means that all changes made by test will rolled back after the test execution.

3. Alternatives

For efficient unit testing, the RxMicro framework supports the mechanism of alternatives.

Alternatives are test components, usually being mocks with predefined behaviors, that are injected by the RxMicro framework into the tested classes. Alternatives are a powerful mechanism for writing unit tests.

The RxMicro framework supports alternatives only for REST-based microservice tests and component unit tests.

When developing a microservice project, two types of components are distinguished:

  • RxMicro component - a class that is part of the RxMicro framework (for example, HttpClientFactory) or a class generated by the RxMicro Annotation Processor (Data Repository, Rest client, etc).

  • Custom component - a developer-written class that is part of a microservice project.

These two types of components have different life cycles:

  • The instances of the RxMicro components are created in the classes generated by the RxMicro Annotation Processor, and are registered in the runtime container. When a reference to the RxMicro component is required, the custom class requests it in the runtime container.

  • The instances of custom components are created independently by the developer in the code.

Due to the difference in life cycles between the two types of the RxMicro components, the RxMicro framework also supports two types of alternatives:

  • alternatives of the RxMicro components;

  • alternatives of custom components.

These types of alternatives differ in the algorithms of injection into the tested class.

3.1. Injection Algorithm for the Alternative of the RxMicro Component

To inject the alternative of the RxMicro component, the RxMicro framework uses the following algorithm:

  1. The alternative instance is created by the developer in the test code or by the testing framework automatically.

  2. Once all alternatives have been created, they are registered in the runtime container.

  3. Once all alternatives have been registered, the RxMicro framework creates an instance of the tested class.

  4. In the constructor or static section of the tested class, a request to the runtime container to get a reference to the RxMicro component is executed.

  5. Since the runtime container already contains an alternative instead of the real component, the alternative is injected into the instance of the tested class.

  6. After initialization, the instance of the tested class contains references to alternatives instead of the real RxMicro components.

3.2. Injection Algorithm for the Alternative of the Custom Component

To inject the alternative of the custom component, the RxMicro framework uses the following algorithm:

  1. The alternative instance is created by the developer in the test code or by the testing framework automatically.

  2. The RxMicro framework creates an instance of the tested class.

  3. In the constructor or static section of the tested class, instances of the real custom components are created.

  4. After initialization, the instance of the tested class contains references to the real custom components.

  5. After creating an instance of the tested class, the RxMicro framework injects the custom component alternatives using the reflection mechanism.
    (I.e. the alternatives replace the real instances already after creating an instance of the tested class.);

  6. After alternative injection, the instance of the tested class contains references to the alternatives of the RxMicro components instead of the real RxMicro components.
    (The real component instances will be removed by the garbage collector later.);

Thus, the main difference of the injection algorithm for the custom component alternatives is that during the injection process, the real component instances are always created.

If the real component creates a reference to an external resource, then this resource will not be released automatically when injecting the alternative!

It is recommended to use the rxmicro.cdi module to create the custom component alternatives that work with external resources.

If the rxmicro.cdi module is used by the developer to create the custom component instances, then all custom components are injected using the algorithm of the RxMicro component alternative injection.

3.3. Alternative Usage

The alternative mechanism is a universal tool that can be applied to the simplest project, which doesn’t use the RxMicro Annotations.
Let’s look at the project consisting of two components: the ChildComponent interface and the ParentComponent class:

public interface ChildComponent {

    String getValue();
}
public final class ParentComponent {

    private final ChildComponent childComponent = () -> "production"; (1)

    public String getEnvironment() {
        return childComponent.getValue();
    }
}
1 The ParentComponent class depends on the ChildComponent.
(This dependency specified explicitly in the source code.)

When writing a unit test for the ParentComponent, it is necessary to replace the real ChildComponent with a mock. Otherwise, it wouldn’t be a unit test, but an integration one.

For this replacement, it is most convenient to use the alternative:

(1)
@RxMicroComponentTest(ParentComponent.class)
final class ParentComponent1Test {

    (3)
    @Alternative
    private final ChildComponent childComponent = () -> "test"; (4)

    private ParentComponent parentComponent; (2)

    @Test
    void Should_use_alternative() {
        assertEquals("test", parentComponent.getEnvironment()); (5)
    }
}
1 The alternatives are supported by the RxMicro framework only during component unit tests or REST-based microservice tests. Therefore, it must be declared that this test is a test of the ParentComponent component.
2 The instance of the tested component will be created by the RxMicro framework automatically.
(The instance is created using the reflection mechanism, so the tested class must contain an available constructor without parameters.)
In order to invoke any method of the tested component, a reference to that component is required. Therefore, the RxMicro framework requires that the developer declares an uninitialized field of the tested component. After starting the test, a reference to the instance of the tested component will be injected into this field using the reflection mechanism.
3 In order to use the alternative mechanism, it is necessary to declare test field as alternative by using the @Alternative annotation.
4 Alternative is a test component with predefined behavior. Therefore, it is necessary to define what value should be returned when invoking the getValue method.
5 When testing the getEnvironment method, the alternative method is invoked instead of the real component one.

If You would like to inject the alternative to the final field, don’t forget to configure maven-surefire-plugin:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>
            --add-opens java.base/java.lang.reflect=rxmicro.reflection (1)
        </argLine>
    </configuration>
</plugin>
1 - It is necessary to add this opens instruction.

When using alternatives it is very convenient to use a dynamic class with programmable behavior. For this purpose, it is very convenient to use the Mockito framework:

(2)
@InitMocks
@RxMicroComponentTest(ParentComponent.class)
final class ParentComponent2Test {

    private ParentComponent parentComponent;

    (1)
    @Mock
    @Alternative
    private ChildComponent childComponent;

    @Test
    void Should_use_alternative() {
        when(childComponent.getValue()).thenReturn("test"); (3)
        assertEquals("test", parentComponent.getEnvironment()); (4)
    }
}
1 To create a mock instance, it is necessary to use the @Mock annotation.
2 In order for JUnit to handle all fields annotated by the @Mock annotation before invoking test methods, it is necessary to annotate the test class by the @InitMocks annotation.
3 Before testing, it is necessary to program the behavior of the getValue method of the declared mock.
4 When testing the getEnvironment method, the method from the alternative is invoked instead of the real component one.

If You would like to inject the alternative to the final field, don’t forget to configure maven-surefire-plugin:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>
            --add-opens java.base/java.lang.reflect=rxmicro.reflection (1)
        </argLine>
    </configuration>
</plugin>
1 - It is necessary to add this opens instruction.

When creating mock alternatives, the @InitMocks annotation should be over the @RxMicroComponentTest annotation (or @RxMicroRestBasedMicroServiceTest when writing REST-based microservice tests), otherwise, the alternative will be injected before creating a mock instance, (i.e. injection of the null instance), which will cause an error!

The project source code used in the current subsection is available at the following link:

3.4. Components with Custom Constructors

In case the custom component does not contain an available constructor without parameters:

public final class ParentComponent {

    private final String prefix;

    private final ChildComponent childComponent = () -> "production"; (1)

    public ParentComponent(final String prefix) { (2)
        this.prefix = requireNonNull(prefix);
    }

    public String getEnvironment() {
        return prefix + " " + childComponent.getValue();
    }
}
1 The ParentComponent depends on the ChildComponent.
(This dependency is specified explicitly in the source code.)
2 When creating the ParentComponent class instance in the constructor, the value of the prefix parameter must be passed.

then the RxMicro framework won’t be able to create an instance of this class.

Therefore, the developer should create an instance of this class in one of the following methods: @BeforeEach, @BeforeThisTest or @BeforeIterationMethodSource:

@InitMocks
@RxMicroComponentTest(ParentComponent.class)
final class ParentComponentTest {

    private ParentComponent parentComponent;

    (2)
    @Mock
    @Alternative
    private ChildComponent childComponent;

    @BeforeEach
    void beforeEach() {
        parentComponent = new ParentComponent("prefix"); (1)
    }

    @Test
    void Should_use_alternative() {
        when(childComponent.getValue()).thenReturn("test");
        assertEquals("prefix test", parentComponent.getEnvironment()); (3)
    }
}
1 The ParentComponent class instance is created inside the method annotated by the @BeforeEach annotation.
2 The childComponent alternative will be injected into the ParentComponent class instance after invoking the beforeEach() method and before the Should_use_alternative() test method.
3 When testing the getEnvironment method, the alternative method is invoked instead of the real component one.

If You would like to inject the alternative to the final field, don’t forget to configure maven-surefire-plugin:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>
            --add-opens java.base/java.lang.reflect=rxmicro.reflection (1)
        </argLine>
    </configuration>
</plugin>
1 - It is necessary to add this opens instruction.

The project source code used in the current subsection is available at the following link:

3.5. Ambiguity Resolving

The alternative created by the developer can be injected by the RxMicro framework not only in the tested component, but also in any of its child components.

During such injection, the ambiguity problem may occur.

3.5.1. Ambiguity Resolving Demonstration

Let’s assume there is some business service in the project:

public interface BusinessService {

    String getValue();
}

This business service is a dependency for three interdependent components: Child, Parent, GrandParent:

public final class Child {

    private final BusinessService childBusinessService = () -> "Child";

    public String getValue() {
        return childBusinessService.getValue();
    }
}
public final class Parent {

    private final Child child = new Child();

    private final BusinessService parentBusinessService = () -> "Parent";

    public String getValue() {
        return parentBusinessService.getValue() + " : " + child.getValue();
    }
}
public final class GrandParent {

    private final Parent parent = new Parent();

    private final BusinessService grandParentBusinessService = () -> "GrandParent";

    public String getValue() {
        return grandParentBusinessService.getValue() + " : " + parent.getValue();
    }
}

When invoking the GrandParent.getValue method, this method is invoked on the business services of all Child, Parent and GrandParent dependent components according to the dependency hierarchy:

final class GrandParent1Test {

    private final GrandParent grandParent = new GrandParent();

    @Test
    void Should_return_default_values() {
        assertEquals("GrandParent : Parent : Child", grandParent.getValue());
    }
}

When using an alternative, the behavior of the GrandParent.getValue method is changed:

@InitMocks
@RxMicroComponentTest(GrandParent.class)
final class GrandParent2Test {

    private GrandParent grandParent;

    @Mock
    (1)
    @Alternative
    private BusinessService businessService;

    @Test
    void Should_inject_alternatives_correctly() {
        when(businessService.getValue()).thenReturn("Mock"); (2)

        assertEquals("Mock : Mock : Mock", grandParent.getValue()); (3)
    }
}
1 An alternative to the business service is created.
2 Before testing, the behavior of the getValue method of created mock is programmed.
3 As a result of the test, You can see that this alternative is injected into all Child, Parent and GrandParent dependent components.

If You would like to inject the alternative to the final field, don’t forget to configure maven-surefire-plugin:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>
            --add-opens java.base/java.lang.reflect=rxmicro.reflection (1)
        </argLine>
    </configuration>
</plugin>
1 - It is necessary to add this opens instruction.

If You create 2 or more (no more than 3 in this test example) alternatives, then each alternative can be injected in a separate business component:

@InitMocks
@RxMicroComponentTest(GrandParent.class)
final class GrandParent3Test {

    private GrandParent grandParent;

    @Mock
    @Alternative
    private BusinessService grandParentBusinessService;

    @Mock
    @Alternative(name = "childBusinessService")
    private BusinessService businessService;

    @Test
    void Should_inject_alternatives_correctly() {
        when(grandParentBusinessService.getValue()).thenReturn("GrandParentMock");
        when(businessService.getValue()).thenReturn("ChildMock");

        assertEquals("GrandParentMock : Parent : ChildMock", grandParent.getValue()); (1)
    }
}
1 The grandParentBusinessService alternative is injected into the GrandParent component, and the businessService alternative is injected into the Child component;

If You would like to inject the alternative to the final field, don’t forget to configure maven-surefire-plugin:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>
            --add-opens java.base/java.lang.reflect=rxmicro.reflection (1)
        </argLine>
    </configuration>
</plugin>
1 - It is necessary to add this opens instruction.

The project source code used in the current subsection is available at the following link:

3.5.2. Ambiguity Resolving Algorithm

To resolve ambiguities, the RxMicro framework uses the following algorithm:

  1. For each tested component, a search for injection candidates is performed.

  2. As a result, a map is formed with a user type as its key and a list of candidates for injection as its value. (The RxMicro framework does not support polymorphism rules when injecting alternatives. Thus, the alternative of the A type can only be injected in the field with the A type).

  3. After receiving a map with candidates for injection, the RxMicro framework passes through this map.

  4. For each user type, a list of candidates and a list of alternatives is requested.

  5. If there is only one alternative and only one candidate for the user type, the RxMicro framework injects this alternative into the candidate field;

  6. If more than one alternative and only one candidate is found, the RxMicro framework will throw out an error;

  7. If there is more than one candidate and only one alternative, then:

    1. The RxMicro framework analyzes the injection candidate field name:

      1. if the candidate field name matches the alternative field name, the RxMicro framework injects this alternative;

      2. if the candidate field name matches the value of the name parameter of the @Alternative annotation, the RxMicro framework injects this alternative;

      3. otherwise this candidate will be skipped;

    2. if no alternative has been injected, the RxMicro framework injects this alternative in all candidate fields.
      (This is the behavior that occurs in the GrandParent2Test test.)

  8. If there is more than one candidate and more than one alternative, then:

    1. The RxMicro framework analyzes the injection candidate field name:

      1. if the candidate field name matches the alternative field name, the RxMicro framework injects this alternative;
        (In the GrandParent3Test test, the grandParentBusinessService alternative is injected in the GrandParent component field, because the names of the alternative and component fields are equal.);

      2. if the candidate field name matches the value of the name parameter of the @Alternative annotation, the RxMicro framework injects this alternative;
        (In the GrandParent3Test test, the businessService alternative is injected in the Child component field, because the name parameter of the @Alternative annotation is equal to the childBusinessService. And in the Child class, the field name with the BusinessService type is also equal to the childBusinessService.)

      3. otherwise this candidate will be skipped;

    2. (When more than one candidate and more than one alternative is found, it is possible that none of the alternatives will be injected.)

3.6. CDI Beans Alternatives

If the developer uses the rxmicro.cdi module in the project, then all custom components are considered as beans and follow the injection algorithm for the alternatives of the RxMicro components.

When using the rxmicro.cdi module, You must always inject dependencies using the CDI mechanism only:

public final class ParentComponent {

    @Inject
    ChildComponent childComponent; (1)

    @Inject
    ChildComponentImpl childComponentImpl; (1)

    public String getEnvironment() {
        return childComponent.getValue() + " " + childComponentImpl.getValue();
    }
}
1 The ParentComponent class depends on the ChildComponent and ChildComponentImpl components.
(These dependencies are injected using the CDI mechanisms.)

The above example is a demonstration of the features of alternatives of custom components if rxmicro.cdi module enabled. That’s why the ChildComponentImpl implementation is injected in the ParentComponent component. In real projects, it’s recommended to inject only interfaces to ensure greater flexibility.

Please note that since the ChildComponentImpl class implements the ChildComponent interface, and all CDI beans are singletons, the childComponent and childComponentImpl fields will contain references to the same ChildComponentImpl instance!

public final class ChildComponentImpl implements ChildComponent {

    public ChildComponentImpl() {
        System.out.println("ChildComponentImpl created"); (1)
    }

    @Override
    public String getValue() {
        return "production";
    }
}
1 When creating the ChildComponentImpl instance, an information message is displayed in the console.
(This message is required to ensure that no real custom instance is created when CDI bean alternatives are used!)

When testing, if no alternatives are created, the tested component uses the real custom component instances:

@InitMocks
@RxMicroComponentTest(ParentComponent.class)
final class ParentComponent1Test {

    private ParentComponent parentComponent;

    private SystemOut systemOut;

    @Test
    void Should_use_alternatives() {
        assertEquals("production production", parentComponent.getEnvironment());              (1)
        assertEquals("ChildComponentImpl created", systemOut.asString()); (2)
    }
}
1 When invoking the getEnvironment method, the real instances of custom components are used.
2 When starting the test, only one ChildComponentImpl class instance is created.

When testing, if alternatives are created, the tested component uses them instead of the real custom component instances:

@InitMocks
@RxMicroComponentTest(ParentComponent.class)
final class ParentComponent2Test {

    private ParentComponent parentComponent;

    @Mock
    @Alternative
    private ChildComponent childComponent;

    @Mock
    @Alternative
    private ChildComponentImpl childComponentImpl;

    private SystemOut systemOut;

    @Test
    void Should_use_alternatives() {
        when(childComponent.getValue()).thenReturn("mock");
        when(childComponentImpl.getValue()).thenReturn("mock");

        assertEquals("mock mock", parentComponent.getEnvironment());   (1)
        assertTrue(
                systemOut.isEmpty(),   (2)
                format("Output not empty: '?'", systemOut.asString())
        );
    }
}
1 When invoking the getEnvironment method, the alternatives of the real custom component instances are used.
2 When starting the test, the ChildComponentImpl class instance is not created.

When using alternatives for complex components, it is possible to use alternatives together with real components:

@InitMocks
@RxMicroComponentTest(ParentComponent.class)
final class ParentComponent3Test {

    private ParentComponent parentComponent;

    @Mock
    @Alternative
    private ChildComponent childComponent;

    private SystemOut systemOut;

    @Test
    void Should_use_alternatives() {
        when(childComponent.getValue()).thenReturn("mock");

        assertEquals("mock production", parentComponent.getEnvironment());                    (1)
        assertEquals("ChildComponentImpl created", systemOut.asString()); (2)
    }
}
1 When invoking the getEnvironment method, an alternative and a real instance are used.
2 When starting the test, only one ChildComponentImpl class instance is created.

The project source code used in the current subsection is available at the following link:

4. How It Works

Java 9 has introduced the JPMS.

This system requires that a developer defines the module-info.java descriptor for each project. In this descriptor, the developer must describe all the dependencies of the current project. In the context of the unit module system, the tests required for each project should be configured as a separate module, since they depend on libraries that should not be available in the runtime. Usually such libraries are unit testing libraries (e.g. JUnit 5), mock creation libraries (e.g. Mockito), etc.

When trying to create a separate module-info.java descriptor available only for unit tests, many modern IDEs report an error.

Therefore, the simplest and most common solution to this problem is to organize unit tests in the form of automatic module.
This solution allows You to correct compilation errors, but when starting tests, there will be runtime errors.
To fix runtime errors, when starting the Java virtual machine, You must add options that configure the Java module system at runtime.

In case the tests are run, these options must be added to the maven-surefire-plugin:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.1</version>
    <configuration>
        <argLine>
            @{argLine}
            --add-exports ...
            --add-opens ...
            --patch-module ...
            --add-modules ...
            --add-reads ...
        </argLine>
    </configuration>
</plugin>

The specified configuration options for the Java module system at runtime can also be added using the features of the java.lang.Module class.

In order the developer is relieved of the need to add the necessary options to the maven-surefire-plugin configuration, the RxMicro framework provides a special io.rxmicro.annotation.processor.RxMicroTestsAnnotationProcessor component.

To activate this component, it is necessary to add a new execution to the maven-compiler-plugin configuration:

<execution>
    <id>test-compile</id>
    <goals>
        <goal>testCompile</goal> (1)
    </goals>
    <configuration>
        <annotationProcessors>
            <annotationProcessor>
                io.rxmicro.annotation.processor.RxMicroTestsAnnotationProcessor (2)
            </annotationProcessor>
        </annotationProcessors>
        <generatedTestSourcesDirectory>
            ${project.build.directory}/generated-test-sources/  (3)
        </generatedTestSourcesDirectory>
    </configuration>
</execution>
1 The separate configuration is required for the tests, so a new execution must be added.
2 The annotation processor class that handles the test configuration.
3 Location of Java classes generated by the RxMicro Test Annotation Processor.

This annotation processor generates one single rxmicro.$$ComponentTestFixer class, that automatically opens access to all packages of the current project to unnamed modules:

public final class $$ComponentTestFixer {

    static {
        final Module currentModule = $$ComponentTestFixer.class.getModule();
        currentModule.addExports("rxmicro", RX_MICRO_REFLECTION_MODULE);
    }

    public $$ComponentTestFixer() {
        final Module currentModule = getClass().getModule();
        if (currentModule.isNamed()) {
            logInfoTestMessage("Fix the environment for componnet test(s)...");
            final Module unnamedModule = getClass().getClassLoader().getUnnamedModule(); (1)
            final Set<Module> modules = unmodifiableOrderedSet(
                    unnamedModule, RX_MICRO_REFLECTION_MODULE
            );
            for (final Module module : modules) {
                for (final String packageName : currentModule.getPackages()) {
                    currentModule.addOpens(packageName, module); (2)
                    logInfoTestMessage(
                            "opens ?/? to ?", (3)
                            currentModule.getName(),
                            packageName,
                            module.isNamed() ? module.getName() : "ALL-UNNAMED"
                    );
                }
            }
        }
    }
}
1 Using the standard Java API, the RxMicro framework retrieves the references to the current and unnamed modules.
2 Using the features of the java.lang.Module class, the RxMicro framework opens the full access to all classes from all packages from the current module.
3 To inform the developer about the successful performance of the rxmicro.$$ComponentTestFixer class, the RxMicro framework displays to the console the information that access was successfully provided.

When running different types of tests, sometimes a different configuration of the Java module system is required. Thus, for each type of test, the RxMicro framework creates a separate class in the rxmicro system package:

Table 2. Names of generated classes
Test type Name of the generated class

REST-based microservice test

$$RestBasedMicroServiceTestFixer

Component unit test

$$ComponentTestFixer

REST-based microservice integration test

$$IntegrationTestFixer

Before starting tests, the RxMicro framework uses a generated class to configure the module system for the test environment:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
...
[INFO] Fix the environment for component test(s)...
[INFO] opens examples.testing/io.rxmicro.examples.testing to ALL-UNNAMED (1)
[INFO] opens examples.testing/rxmicro to ALL-UNNAMED
[INFO] Running io.rxmicro.examples.testing.ParentComponent1Test (2)
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.278 s
[INFO] Running io.rxmicro.examples.testing.ParentComponent2Test
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.378 s
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
1 All packages of the current module are opened before starting tests.
2 After configuring the module system for the test environment, Unit tests are started.

Thus, for the successful writing of tests using the RxMicro framework, besides adding the required libraries, do not forget to configure the maven-compiler-plugin by adding the following annotation processor for the test environment: io.rxmicro.annotation.processor.RxMicroTestsAnnotationProcessor.

5. The @BeforeThisTest and @BeforeIterationMethodSource Annotations

In order to perform any actions before invoking the test method, the JUnit 5 framework provides a special @BeforeEach annotation.

According to the working rules of the JUnit 5 framework, if the test class contains a method annotated by the @BeforeEach annotation, then this method should be invoked before invoking each test method in the test class.

Let’s consider the possibilities of using the @BeforeEach annotation. We’ll use the example of a project consisting of two components: ChildComponent interface and ParentComponent class.

public interface ChildComponent {

    String getValue();
}
public final class ParentComponent {

    private final ChildComponent childComponent = () -> "production";

    public String getEnvironment() {
        return childComponent.getValue();
    }
}

When using the mock alternative, it is possible to program the behavior in the method annotated by the @BeforeEach annotation:

@InitMocks
@RxMicroComponentTest(ParentComponent.class)
final class ParentComponent1Test {

    private ParentComponent parentComponent;

    @Mock
    @Alternative
    private ChildComponent childComponent;

    (1)
    @BeforeEach
    void beforeEach() {
        when(childComponent.getValue()).thenReturn("mock");
    }

    @Test
    void Should_use_alternative() {
        assertEquals("mock", parentComponent.getEnvironment());
    }
}
1 Since the beforeEach() method will be surely invoked by the JUnit 5 framework before invoking the Should_use_alternative() test method, the programmed mock behavior will be used during the test;

The weak point in using the method annotated by the @BeforeEach annotation, is that it is impossible to program different behavior for two or more test methods in the same class:

@BeforeEach
void beforeEach() {
    when(childComponent.getValue()).thenReturn("mock");
}

@Test
void Should_use_alternative1() {
    assertEquals("mock1", parentComponent.getEnvironment()); // failed
}

@Test
void Should_use_alternative2() {
    assertEquals("mock2", parentComponent.getEnvironment()); // failed
}

The standard solution to this problem consists in programming behavior in the test method:

@Test
void Should_use_alternative1() {
    when(childComponent.getValue()).thenReturn("mock1");
    assertEquals("mock1", parentComponent.getEnvironment());
}

@Test
void Should_use_alternative2() {
    when(childComponent.getValue()).thenReturn("mock2");
    assertEquals("mock2", parentComponent.getEnvironment());
}

However, the alternative configuration in the test method is not always convenient.
This can cause a problem, especially when it is necessary to configure the RxMicro component alternative before its registration in the runtime container!

To solve this problem, the RxMicro framework provides two additional annotations:

The @BeforeThisTest annotation allows You to specify the method to be invoked before invoking a specific test method:

@InitMocks
@RxMicroComponentTest(ParentComponent.class)
final class ParentComponent4Test {

    private ParentComponent parentComponent;

    @Mock
    @Alternative
    private ChildComponent childComponent;

    void beforeTest1() { (1)
        when(childComponent.getValue()).thenReturn("mock1");
    }

    @Test
    (2)
    @BeforeThisTest(method = "beforeTest1")
    void Should_use_alternative1() {
        assertEquals("mock1", parentComponent.getEnvironment());
    }

    void beforeTest2() {
        when(childComponent.getValue()).thenReturn("mock2");
    }

    @Test
    @BeforeThisTest(method = "beforeTest2")
    void Should_use_alternative2() {
        assertEquals("mock2", parentComponent.getEnvironment());
    }
}
1 To configure the mock alternative, it is necessary to create a method in the test class.
2 If this method needs to be invoked before the test method, it is necessary to specify the name of this method using the @BeforeThisTest annotation.
(The @BeforeThisTest annotation must annotate the test method!).

When creating parametrized tests, it is necessary to use the @BeforeIterationMethodSource annotation instead of the @BeforeThisTest annotation:

@InitMocks
@RxMicroComponentTest(ParentComponent.class)
final class ParentComponent5Test {

    private ParentComponent parentComponent;

    @Mock
    @Alternative
    private ChildComponent childComponent;

    void beforeEachPreparer1() {
        when(childComponent.getValue()).thenReturn(new BigDecimal("23").toString());
    }

    void beforeEachPreparer2() {
        when(childComponent.getValue()).thenReturn("23");
    }

    @ParameterizedTest
    (1)
    @BeforeIterationMethodSource(methods = {
            "beforeEachPreparer1",
            "beforeEachPreparer2"
    })
    void Should_use_alternative2(final String method) { (2)
        assertEquals("23", parentComponent.getEnvironment());
    }
}
1 Using the @BeforeIterationMethodSource annotation, the developer specifies an array of methods. Each method in this array must be invoked before executing a new iteration of the parameterized test.
2 For reporting it is recommended to specify the final String method parameter of the parameterized test. This parameter is not used in the test, but if it is present, the JUnit 5 framework automatically passes the method name to it. Thus, in the execution report of the parameterized test, for each iteration the method that was invoked before execution of this iteration will be specified.

The project source code used in the current subsection is available at the following link:

6. REST-based Microservice Testing

The REST-based microservice test is a standard Unit test that tests only the source code of a microservice. If the current microservice depends on external services (e.g. database, other REST-based microservices, etc.), then it is allowed to use mocks for these external services during its testing. If several REST-based microservices need to be tested, it is recommended to use the REST-based microservice integration testing.

For easy writing of microservice tests, the RxMicro framework provides:

  1. The additional @RxMicroRestBasedMicroServiceTest annotation, that informs the RxMicro framework about the need to start the tested REST-based microservice and prepare the environment to execute test HTTP requests.

  2. A special blocking HTTP client to execute HTTP requests during testing: BlockingHttpClient.

  3. The SystemOut interface for easy console access.

For each microservice test, the RxMicro framework performs the following actions:

  1. Before starting all the test methods in the class:

    1. checks the test class for compliance with the rules of REST-based microservice testing defined by the RxMicro framework;

    2. starts an HTTP server on a random free port;

    3. creates an instance of the BlockingHttpClient type;

    4. connects the created BlockingHttpClient to the running HTTP server.

  2. Before starting each test method:

    1. if necessary, invokes the methods defined using the @BeforeThisTest or @BeforeIterationMethodSource annotations;

    2. if necessary, registers the RxMicro component alternatives in the RxMicro container;

    3. registers the tested REST-based microservice on the running HTTP server;

    4. if necessary, injects the custom component alternatives to the REST-based microservice;

    5. injects a reference to the BlockingHttpClient instance into the test class;

    6. if necessary, creates the System.out mock, and injects it into the test class.

  3. After performing each test method:

    1. deletes all registered components from the RxMicro container;

    2. deletes all registered REST-based microservices on the running HTTP server;

    3. if necessary, restores the System.out.

  4. After performing all the tests in the class:

    1. clears the resources of the BlockingHttpClient component;

    2. stops the HTTP server and releases the selected resources.

6.1. Basic Principles

To understand the REST-based microservice testing principles, let’s create the simplest microservice that returns the "Hello World!" message.

Since the microservice will return a JSON object, it is necessary to create a response model:

public final class Response {

    final String message;

    public Response(final String message) {
        this.message = requireNonNull(message);
    }
}

When there is the GET request to the microservice, it should return the "Hello World!" message:

final class MicroService {

    @GET("/")
    CompletableFuture<Response> get() {
        return completedFuture(new Response("Hello World!"));
    }
}

The testing process of the REST-based microservice is to perform an HTTP request after the RxMicro framework starts the tested microservice on the HTTP server. After receiving a response from the microservice, this response is compared to the expected response:

(1)
@RxMicroRestBasedMicroServiceTest(MicroService.class)
class MicroServiceTest {

    private BlockingHttpClient blockingHttpClient; (2)

    @Test
    void Should_handle_GET_request() {
        final ClientHttpResponse response = blockingHttpClient.get("/");

        assertEquals(jsonObject("message", "Hello World!"), response.getBody()); (3)
        assertEquals(200, response.getStatusCode());
    }
}
1 To start the HTTP server and register the tested REST-based microservice, it is necessary to annotate the test class by the @RxMicroRestBasedMicroServiceTest annotation. In the parameter of this annotation it is specified which REST-based microservice class will be tested in the current test.
2 To execute blocking HTTP requests, the RxMicro framework supports the special BlockingHttpClient component. The developer must declare a reference to this component, and while starting the test, the RxMicro framework will automatically inject the created BlockingHttpClient class instance, using the reflection mechanism.
3 Upon receiving the HTTP response from the microservice, the developer should compare the response body with the expected result in the test.

To get additional info about writing tests that require JSON object comparison, please read rxmicro.json Module Usage section.

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

6.2. Features of Testing Complex Microservices that use Alternatives

6.2.1. Features of REST Client Testing

This section will cover the features of testing REST-based microservices that use REST clients.

The source code of such REST-based microservice consists of the Response model class, ExternalMicroService REST client and ConsumeMicroService HTTP request handler:

public final class Response {

    String message;

    public Response(final String message) {
        this.message = requireNonNull(message);
    }

    public Response() {
    }
}
@RestClient
public interface ExternalMicroService {

    @GET("/")
    CompletableFuture<Response> get();
}
final class MicroService {

    private final ExternalMicroService externalMicroService =
            getRestClient(ExternalMicroService.class);

    @GET("/")
    CompletableFuture<Response> get() {
        return externalMicroService.get();
    }
}

The most logical way to test such microservice is to create a mock alternative for the ExternalMicroService component:

@InitMocks
@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceBusinessLogicOnlyTest {

    private BlockingHttpClient blockingHttpClient;

    @Mock
    @Alternative
    private ExternalMicroService externalMicroService;

    @Test
    void Should_delegate_call_to_ExternalMicroService() {
        when(externalMicroService.get()).thenReturn(completedFuture(new Response("mock")));

        final ClientHttpResponse response = blockingHttpClient.get("/");

        assertEquals(jsonObject("message", "mock"), response.getBody());
    }
}

To get additional info about writing tests that require JSON object comparison, please read rxmicro.json Module Usage section.

But if on the basis of such test we build the source code coverage report, this report will show a low degree of coverage:

jacoco business logic only
Figure 1. The test coverage report when using a mock alternative for REST client.

Such a result is caused by the fact that after creating a mock alternative for the ExternalMicroService component, the classes generated by the RxMicro framework are not used in the testing process for the REST client work.

If such a result is not acceptable, it is necessary to:

  1. create a mock alternative to the HttpClientFactory RxMicro component;

  2. use the static methods of the HttpClientMockFactory class to program the mock behavior.

@InitMocks
@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceWithAllGeneratedCodeTest {

    private BlockingHttpClient blockingHttpClient;

    @Mock
    @Alternative
    private HttpClientFactory httpClientFactory;

    void prepareExternalMicroServiceHttpClient() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(GET)
                        .setPath("/")
                        .build(),
                jsonObject("message", "mock")
        );
    }

    @Test
    @BeforeThisTest(method = "prepareExternalMicroServiceHttpClient")
    void Should_delegate_call_to_ExternalMicroService() {
        final ClientHttpResponse response = blockingHttpClient.get("/");

        assertEquals(jsonObject("message", "mock"), response.getBody());
    }
}

The modified test shows a coverage rate of 100%:

jacoco all generated
Figure 2. The test coverage report when using the HttpClientFactory mock alternative.

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

6.2.2. Features of Testing Mongo Repositories

This section will cover the features of testing REST-based microservices that use mongo repositories.

The source code of such REST-based microservice consists of the Entity entity model, Response model class, DataRepository mongo repository and ConsumeMicroService HTTP request handler:

public final class Response {

    final String message;

    public Response(final String message) {
        this.message = requireNonNull(message);
    }
}
public final class Entity {

    String data;

    public String getData() {
        return data;
    }
}
@MongoRepository(collection = "collection")
public interface DataRepository {

    @Find(query = "{_id: ?}")
    CompletableFuture<Optional<Entity>> findById(long id);
}
final class MicroService {

    private final DataRepository dataRepository =
            getRepository(DataRepository.class);

    @GET("/")
    CompletableFuture<Optional<Response>> get(final Long id) {
        return dataRepository.findById(id).thenApply(optionalEntity ->
                optionalEntity.map(entity ->
                        new Response(entity.getData())));
    }
}

The most logical way to test such microservice is to create a mock alternative for the DataRepository component:

@InitMocks
@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceBusinessLogicOnlyTest {

    private BlockingHttpClient blockingHttpClient;

    @Mock
    @Alternative
    private DataRepository dataRepository;

    @Mock
    private Entity entity;

    @Test
    void Should_return_Entity_data() {
        when(entity.getData())
                .thenReturn("data");
        when(dataRepository.findById(1))
                .thenReturn(completedFuture(Optional.of(entity)));

        final ClientHttpResponse response = blockingHttpClient.get("/?id=1");

        assertEquals(jsonObject("message", "data"), response.getBody());
        assertEquals(200, response.getStatusCode());
    }

    @Test
    void Should_return_Not_Found_error() {
        when(dataRepository.findById(1))
                .thenReturn(completedFuture(Optional.empty()));

        final ClientHttpResponse response = blockingHttpClient.get("/?id=1");

        assertEquals(jsonObject("message", "Not Found"), response.getBody());
        assertEquals(404, response.getStatusCode());
    }
}

To get additional info about writing tests that require JSON object comparison, please read rxmicro.json Module Usage section.

But if on the basis of such test we build the source code coverage report, this report will show a low degree of coverage:

jacoco business logic only
Figure 3. The test coverage report when using a mock alternative for Mongo repository.

Such a result is caused by the fact that after creating a mock alternative for the DataRepository component, the classes generated by the RxMicro framework are not used in the testing process for the mongo repository work.

If such a result is not acceptable, it is necessary to:

  1. create a mock alternative to the MongoDatabase RxMicro component;

  2. use the static methods of the MongoMockFactory class to program the mock behavior.

@InitMocks
@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceWithAllGeneratedCodeTest {

    private static final FindOperationMock FIND_OPERATION_MOCK =
            new FindOperationMock.Builder()
                    .setAnyQuery()
                    //.setQuery("{_id: 1}")
                    .build();

    private BlockingHttpClient blockingHttpClient;

    @Mock
    @Alternative
    private MongoDatabase mongoDatabase;

    void prepareOneEntityFound() {
        prepareMongoOperationMocks(
                mongoDatabase,
                "collection",
                FIND_OPERATION_MOCK,
                new Document("data", "data")
        );
    }

    @Test
    @BeforeThisTest(method = "prepareOneEntityFound")
    void Should_return_Entity_data() {
        final ClientHttpResponse response = blockingHttpClient.get("/?id=1");

        assertEquals(jsonObject("message", "data"), response.getBody());
        assertEquals(200, response.getStatusCode());
    }

    void prepareNoEntityFound() {
        prepareMongoOperationMocks(
                mongoDatabase,
                "collection",
                FIND_OPERATION_MOCK
        );
    }

    @Test
    @BeforeThisTest(method = "prepareNoEntityFound")
    void Should_return_Not_Found_error() {
        final ClientHttpResponse response = blockingHttpClient.get("/?id=1");

        assertEquals(jsonObject("message", "Not Found"), response.getBody());
        assertEquals(404, response.getStatusCode());
    }
}

The modified test shows a coverage rate of 100%:

jacoco all generated
Figure 4. The test coverage report when using the MongoDatabase mock alternative.

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

6.2.3. Features of Testing Postgres Repositories

This section will cover the features of testing REST-based microservices that use postgres repositories.

The source code of such REST-based microservice consists of the Entity entity model, Response model class, DataRepository mongo repository and ConsumeMicroService HTTP request handler:

public final class Response {

    final String message;

    public Response(final String message) {
        this.message = requireNonNull(message);
    }
}
@Table
public final class Entity {

    @Column(length = Column.UNLIMITED_LENGTH)
    String data;

    public String getData() {
        return data;
    }
}
@PostgreSQLRepository
public interface DataRepository {

    @Select("SELECT data FROM ${table} WHERE id=?")
    CompletableFuture<Optional<Entity>> findById(long id);
}
final class MicroService {

    private final DataRepository dataRepository = getRepository(DataRepository.class);

    @GET("/")
    CompletableFuture<Optional<Response>> get(final Long id) {
        return dataRepository.findById(id).thenApply(optionalEntity ->
                optionalEntity.map(entity ->
                        new Response(entity.getData())));
    }
}

The most logical way to test such microservice is to create a mock alternative for the DataRepository component:

@InitMocks
@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceBusinessLogicOnlyTest {

    private BlockingHttpClient blockingHttpClient;

    @Mock
    @Alternative
    private DataRepository dataRepository;

    @Mock
    private Entity entity;

    @Test
    void Should_return_Entity_data() {
        when(entity.getData())
                .thenReturn("data");
        when(dataRepository.findById(1))
                .thenReturn(completedFuture(Optional.of(entity)));

        final ClientHttpResponse response = blockingHttpClient.get("/?id=1");

        assertEquals(jsonObject("message", "data"), response.getBody());
        assertEquals(200, response.getStatusCode());
    }

    @Test
    void Should_return_Not_Found_error() {
        when(dataRepository.findById(1))
                .thenReturn(completedFuture(Optional.empty()));

        final ClientHttpResponse response = blockingHttpClient.get("/?id=1");

        assertEquals(jsonObject("message", "Not Found"), response.getBody());
        assertEquals(404, response.getStatusCode());
    }
}

To get additional info about writing tests that require JSON object comparison, please read rxmicro.json Module Usage section.

But if on the basis of such test we build the source code coverage report, this report will show a low degree of coverage:

jacoco business logic only
Figure 5. The test coverage report when using a mock alternative for Postgresql repository.

Such a result is caused by the fact that after creating a mock alternative for the DataRepository component, the classes generated by the RxMicro framework are not used in the testing process for the postgres repository work.

If such a result is not acceptable, it is necessary to:

  1. create a mock alternative to the ConnectionPool RxMicro component;

  2. use the static methods of the SQLMockFactory class to program the mock behavior.

@InitMocks
@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceWithAllGeneratedCodeTest {

    private static final SQLQueryWithParamsMock SQL_PARAMS_MOCK =
            new SQLQueryWithParamsMock.Builder()
                    .setAnySql()
                    //.setSql("SELECT data FROM entity WHERE id = $1")
                    //.setBindParams(1L)
                    .build();

    private BlockingHttpClient blockingHttpClient;

    @Mock
    @Alternative
    private ConnectionPool connectionPool;

    void prepareOneEntityFound() {
        prepareSQLOperationMocks(
                connectionPool,
                SQL_PARAMS_MOCK,
                "data"
        );
    }

    @Test
    @BeforeThisTest(method = "prepareOneEntityFound")
    void Should_return_Entity_data() {
        final ClientHttpResponse response = blockingHttpClient.get("/?id=1");

        assertEquals(jsonObject("message", "data"), response.getBody());
        assertEquals(200, response.getStatusCode());
    }

    void prepareNoEntityFound() {
        prepareSQLOperationMocks(
                connectionPool,
                SQL_PARAMS_MOCK,
                List.of()
        );
    }

    @Test
    @BeforeThisTest(method = "prepareNoEntityFound")
    void Should_return_Not_Found_error() {
        final ClientHttpResponse response = blockingHttpClient.get("/?id=1");

        assertEquals(jsonObject("message", "Not Found"), response.getBody());
        assertEquals(404, response.getStatusCode());
    }
}

The modified test shows a coverage rate of 100%:

jacoco all generated
Figure 6. The test coverage report when using the ConnectionPool mock alternative.

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

6.3. Custom and the RxMicro Framework Code Execution Order

For efficient writing of Rest-based microservice tests, it is necessary to know the execution order of user, and the RxMicro framework code.

When testing a Rest-based microservice, using the following test:

@InitMocks
@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {

    @BeforeAll
    static void beforeAll() {
    }

    private BlockingHttpClient blockingHttpClient;

    private SystemOut systemOut;

    @Mock
    @Alternative
    private BusinessService businessService;

    public MicroServiceTest() {
    }

    @BeforeEach
    void beforeEach() {
    }

    void beforeTest1UserMethod() {
    }

    @Test
    @BeforeThisTest(method = "beforeTest1UserMethod")
    void test1() {
    }

    void beforeTest2UserMethod() {
    }

    @Test
    @BeforeThisTest(method = "beforeTest2UserMethod")
    void test2() {
    }

    @AfterEach
    void afterEach() {
    }

    @AfterAll
    static void afterAll() {
    }
}

the execution order will be as follows:

RX-MICRO: Test class validated.
RX-MICRO: HTTP server started without any REST-based microservices using random free port.
RX-MICRO: Blocking HTTP client created and connected to the started HTTP server.
USER-TEST: '@org.junit.jupiter.api.BeforeAll' invoked.

USER-TEST: new instance of the REST-based microservice test class created.
MOCKITO: All mocks created and injected.
RX-MICRO: Alternatives of the RxMicro components registered in the RxMicro runtime containers.
RX-MICRO: Blocking HTTP client injected to the instance of the test class.
RX-MICRO: SystemOut instance created and injected to the instance of the test class.
USER-TEST: '@org.junit.jupiter.api.BeforeEach' invoked.
USER-TEST: 'beforeTest1UserMethod' invoked.
RX-MICRO: Current REST-based microservice instance created and registered in the HTTP server.
RX-MICRO: Alternatives of the user components injected to the REST-based microservice instance.
USER-TEST: 'test1()' invoked.
USER-TEST: '@org.junit.jupiter.api.AfterEach' invoked.
RX-MICRO: All registered alternatives removed from the RxMicro runtime containers.
RX-MICRO: Current REST-based microservice instance unregistered from the HTTP server.
RX-MICRO: System.out reset.
MOCKITO: All mocks destroyed.

USER-TEST: new instance of the REST-based microservice test class created.
MOCKITO: All mocks created and injected.
RX-MICRO: Alternatives of the RxMicro components registered in the RxMicro runtime containers.
RX-MICRO: Blocking HTTP client injected to the instance of the test class.
RX-MICRO: SystemOut instance created and injected to the instance of the test class.
USER-TEST: '@org.junit.jupiter.api.BeforeEach' invoked.
USER-TEST: 'beforeTest2UserMethod' invoked.
RX-MICRO: Current REST-based microservice instance created and registered in the HTTP server.
RX-MICRO: Alternatives of the user components injected to the REST-based microservice instance.
USER-TEST: 'test2()' invoked.
USER-TEST: '@org.junit.jupiter.api.AfterEach' invoked.
RX-MICRO: All registered alternatives removed from the RxMicro runtime containers.
RX-MICRO: Current REST-based microservice instance unregistered from the HTTP server.
RX-MICRO: System.out reset.
MOCKITO: All mocks destroyed.

USER-TEST: '@org.junit.jupiter.api.AfterAll' invoked.
RX-MICRO: Blocking HTTP client released.
RX-MICRO: HTTP server stopped.

In the above execution order of user, and the RxMicro framework code the following clarifications are implied:

  1. The MOCKITO prefix means that the action is activated by the @InitMocks annotation.

  2. The RX-MICRO prefix means that the action is activated by the @RxMicroRestBasedMicroServiceTest annotation.

  3. The USER-TEST prefix means that at this stage a custom method from the MicroServiceTest class is invoked.

7. Testing of Microservice Components

7.1. Basic Principles

The basic principles of component testing are covered by the Section 3.3, “Alternative Usage” section.

7.2. Features of Testing Complex Components that Use Alternatives

The features of testing complex components that use alternatives are the same as for REST-based microservice testing.

For more information, we recommend that You familiarize yourself with the following examples:

  • Features of testing components that use REST clients:

  • Features of testing components that use mongo repositories:

  • Features of testing components that use postgres repositories:

7.3. Custom and the RxMicro Framework Execution Order

For efficient writing of component tests, it is necessary to know the execution order of user, and the RxMicro framework code.

When testing a component, using the following test:

@InitMocks
@RxMicroComponentTest(BusinessService.class)
final class BusinessServiceTest {

    @BeforeAll
    static void beforeAll() {
    }

    private BusinessService businessService;

    private SystemOut systemOut;

    @Mock
    @Alternative
    private BusinessService.ChildBusinessService childBusinessService;

    public BusinessServiceTest() {
    }

    @BeforeEach
    void beforeEach() {
    }

    void beforeTest1UserMethod() {
    }

    @Test
    @BeforeThisTest(method = "beforeTest1UserMethod")
    void test1() {
    }

    void beforeTest2UserMethod() {
    }

    @Test
    @BeforeThisTest(method = "beforeTest2UserMethod")
    void test2() {
    }

    @AfterEach
    void afterEach() {
    }

    @AfterAll
    static void afterAll() {
    }
}

the execution order will be as follows:

RX-MICRO: Test class validated.
USER-TEST: '@org.junit.jupiter.api.BeforeAll' invoked.

USER-TEST: new instance of the component test class created.
MOCKITO: All mocks created and injected.
RX-MICRO: Alternatives of the RxMicro components registered in the RxMicro runtime containers.
RX-MICRO: SystemOut instance created and injected to the instance of the test class.
USER-TEST: '@org.junit.jupiter.api.BeforeEach' invoked.
USER-TEST: 'beforeTest1UserMethod' invoked.
RX-MICRO: Tested component instance created, if it is not created by user.
RX-MICRO: Alternatives of the user components injected to the tested component instance.
USER-TEST: 'test1()' method invoked.
USER-TEST: '@org.junit.jupiter.api.AfterEach' invoked.
RX-MICRO: All registered alternatives removed from the RxMicro runtime containers.
RX-MICRO: System.out reset.
MOCKITO: All mocks destroyed.

USER-TEST: new instance of the component test class created.
MOCKITO: All mocks created and injected.
RX-MICRO: Alternatives of the RxMicro components registered in the RxMicro runtime containers.
RX-MICRO: SystemOut instance created and injected to the instance of the test class.
USER-TEST: '@org.junit.jupiter.api.BeforeEach' invoked.
USER-TEST: 'beforeTest2UserMethod' invoked.
RX-MICRO: Tested component instance created, if it is not created by user.
RX-MICRO: Alternatives of the user components injected to the tested component instance.
USER-TEST: 'test2()' method invoked.
USER-TEST: '@org.junit.jupiter.api.AfterEach' invoked.
RX-MICRO: All registered alternatives removed from the RxMicro runtime containers.
RX-MICRO: System.out reset.
MOCKITO: All mocks destroyed.

USER-TEST: '@org.junit.jupiter.api.AfterAll' invoked.

In the above execution order of user, and the RxMicro framework code the following clarifications are implied:

  1. The MOCKITO prefix means that the action is activated by the @InitMocks annotation.

  2. The RX-MICRO prefix means that the action is activated by the @RxMicroComponentTest annotation.

  3. The USER-TEST prefix means that at this stage a custom method from the BusinessServiceTest class is invoked.

8. REST-based Microservice Integration Testing

The REST-based microservice integration testing allows You to test a complete system, which can consist of several REST-based microservices.

For easy writing of the REST-based microservices integration tests, the RxMicro framework provides:

  1. The additional @RxMicroIntegrationTest annotation, that informs the RxMicro framework about the need to create an HTTP client and inject it into the tested class.

  2. A special blocking HTTP client to execute HTTP requests during testing: BlockingHttpClient.

  3. The SystemOut interface for easy console access.

The main differences between integration testing and REST-based microservice testing:

  1. for integration tests, the RxMicro framework does not run an HTTP server;

  2. the developer has to start and stop the system consisting of REST-based microservices;

  3. the RxMicro framework does not support alternatives and additional configuration for integration tests;

8.1. Basic Principles

To demonstrate the features of the integration testing, let’s look at the following microservice:

public final class MicroService {

    @GET("/")
    void handle() {
        System.out.println("handle");
    }
}

The integration test for this microservice will be as follows:

(1)
@RxMicroIntegrationTest
final class MicroServiceIntegrationTest {

    (2)
    private static final int PORT = 55555;

    private static ServerInstance serverInstance;

    (5)
    @BeforeAll
    static void beforeAll() {
        new Configs.Builder()
                .withConfigs(new HttpServerConfig()
                        .setPort(PORT))
                .build(); (4)
        serverInstance = startRestServer(MicroService.class);(3)
    }

    (6)
    @BlockingHttpClientSettings(port = PORT)
    private BlockingHttpClient blockingHttpClient;

    (7)
    private SystemOut systemOut;

    @Test
    void test() {
        final ClientHttpResponse response = blockingHttpClient.get("/");

        assertEquals(200, response.getStatusCode()); (8)
        assertTrue(response.isBodyEmpty(), "Body not empty: " + response.getBody());(8)
        assertEquals("handle", systemOut.asString()); (9)
    }

    @AfterAll
    static void afterAll() throws InterruptedException {
        serverInstance.shutdownAndWait();
    }
}
1 The @RxMicroIntegrationTest annotation informs the RxMicro framework that this test is an integration test.
2 The constant declares the port that will be used to start the HTTP server.
3 The integration test requires the developer to start the HTTP server manually.
4 Before running the HTTP server, You must set the necessary HTTP port.
5 The configuration and start of the HTTP server must be done before running the test method.
6 Since a port other than the standard HTTP port is used, You need to specify which port the BlockingHttpClient component should use for connecting to the HTTP server. The BlockingHttpClient component configuration is performed using the special @BlockingHttpClientSettings annotation.
7 The integration test supports the possibility of creating the System.out mock.
8 After receiving a response from the microservice, ensure that the request has been executed successfully.
9 After receiving a response from the microservice, ensure that the microservice has sent the specified message to the System.out.

This integration test demonstrates all features of the integration test activated by the @RxMicroIntegrationTest annotation.

Thus, the integration test unlike the REST-based microservice test can only inject a blocking HTTP client and create a mock for the System.out.

To get additional info about writing tests that require JSON object comparison, please read rxmicro.json Module Usage section.

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

8.2. The BlockingHttpClient Settings

The BlockingHttpClient component, which is used for writing REST-based microservices and integration tests, is configured by the @BlockingHttpClientSettings annotation:

private static final int SERVER_PORT = getRandomFreePort(); (4)

@BlockingHttpClientSettings(
        schema = HTTPS,                     (1)
        host = "examples.rxmicro.io",       (2)
        port = 9876,                        (3)
        randomPortProvider = "SERVER_PORT", (4)
        versionValue = "v1.1",              (5)
        versionStrategy = HEADER,           (6)
        requestTimeout = 15,                (7)
        followRedirects = Option.ENABLED    (8)
)
private BlockingHttpClient blockingHttpClient;
1 The schema parameter allows You to specify the HTTP protocol schema.
2 The host parameter allows You to specify the remote host on which the microservice is running.
(This parameter allows performing integration testing for remote microservices.)
3 The port parameter allows You to specify the static connection port.
4 The randomPortProvider parameter allows You to specify the dynamic connection port.
(The port will be read from the static final variable of the current class with the SERVER_PORT name.)
(The port and randomPortProvider parameters are mutually exclusive.)
5 The versionValue allows You to specify the microservice version.
6 The versionStrategy parameter allows specifying the versioning strategy, which is used in the tested microservice.
7 The requestTimeout parameter allows specifying the request timeout.
8 The followRedirects parameter returns follow redirect option for the blocking HTTP client

8.3. The docker Usage

To perform the integration testing of microservices, it is convenient to use the docker.

This demonstration example uses the docker image: rxmicro/simple-hello-world.

The source code of the project, on the basis of which this docker image was built, is available at the following link:

To start the docker containers in the integration test it is convenient to use the Testcontainers Java library:

(1)
@RxMicroIntegrationTest
(2)
@Testcontainers
final class HelloWorldMicroService_IT {

    (3)
    @Container
    private static final DockerComposeContainer<?> compose =
            new DockerComposeContainer<>(new File("docker-compose.yml").getAbsoluteFile())
                    .withLocalCompose(true)
                    .withPull(false)
                    .withTailChildContainers(true)
                    .waitingFor("rxmicro-hello-world", Wait.forHttp("/http-health-check"));

    private BlockingHttpClient blockingHttpClient;

    @Test
    void Should_return_Hello_World() {
        final ClientHttpResponse response = blockingHttpClient.get("/");

        assertEquals(jsonObject("message", "Hello World!"), response.getBody()); (4)
        assertEquals(200, response.getStatusCode());
    }
}
1 The @RxMicroIntegrationTest annotation informs the RxMicro framework that this test is an integration test.
2 The @Testcontainers annotation activates the start and stop of the docker containers to be used in this test.
3 The @Container annotation indicates the docker container to be used in this test. (To start microservices in the docker containers the docker-compose utility is used.)
4 During the testing process, ensure that the tested microservice returns the "Hello World!" message.

To start the REST-based microservice in the docker container, the following configuration file for the docker-compose utility is used:

version: '3.7'
services:
  rxmicro-hello-world:
    image: rxmicro/simple-hello-world
    ports:
      - 8080:8080
    healthcheck:
      test: wget http://localhost:8080/http-health-check || exit 1
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

To get additional info about writing tests that require JSON object comparison, please read rxmicro.json Module Usage section.

The project source code used in the current subsection is available at the following link:

9. Database Testing Using DBUnit

DbUnit is an extension targeted at database-driven projects that, among other things, puts your database into a known state between test runs. This is an excellent way to avoid the myriad of problems that can occur when one test case corrupts the database and causes subsequent tests to fail or exacerbate the damage.

9.1. Test Database Configuration

To communicate with test database the RxMicro framework uses the predefined TestDatabaseConfig config class. This class is the usual RxMicro configuration class.

It means that settings for test database can be configured using:

  • test-database.properties classpath resource.

  • ./test-database.properties file.

  • $HOME/test-database.properties file.

  • $HOME/.rxmicro/test-database.properties file.

  • environment variables.

  • Java system properties.

  • @WithConfig annotation.

Besides that the settings for test database can be changed using TestDatabaseConfig.getCurrentTestDatabaseConfig() static method. This approach useful if test database is working at the docker container:

@BeforeEach
void beforeEach() {
    getCurrentTestDatabaseConfig()
            .setHost(postgresqlTestDockerContainer.getContainerIpAddress())
            .setPort(postgresqlTestDockerContainer.getFirstMappedPort());
}

9.2. Retrieve Connection Strategies

The RxMicro framework provides the following retrieve connection to the test database strategies:

  • One connection per all test classes.

  • One connection per test class (default strategy).

  • One connection per test method.

9.2.1. One connection per all test classes

This strategy informs the DBUnit to use single connection per all tests for your project.
The RxMicro team recommends using this strategy for external databases only.

@RxMicroIntegrationTest
@Testcontainers
(1)
@DbUnitTest(retrieveConnectionStrategy = PER_ALL_TEST_CLASSES)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
// FIXME "org.postgresql.util.PSQLException: FATAL: the database system is starting up" and remove @Ignore
@Ignore
final class DbStateUsingPerAllTestClassesConnectionTest {

    private static final int DB_PORT = getRandomFreePort();

    @WithConfig
    private static final TestDatabaseConfig CONFIG = new TestDatabaseConfig()
            .setType(DatabaseType.POSTGRES)
            .setPort(DB_PORT)
            .setUser("rxmicro")
            .setPassword("password")
            .setDatabase("rxmicro");

    @Container
    private static final GenericContainer<?> POSTGRESQL_TEST_DB =
            new FixedHostPortGenericContainer<>("rxmicro/postgres-test-db")
                    .withExposedPorts(5432)
                    .withFixedExposedPort(DB_PORT, 5432);

    @Test
    @ExpectedDataSet("dataset/rxmicro-test-dataset.xml")
    @Order(1)
    void Should_contain_expected_dataset() {

    }

    @Test
    @InitialDataSet("dataset/rxmicro-test-dataset-two-rows-only.xml")
    @ExpectedDataSet("dataset/rxmicro-test-dataset-two-rows-only.xml")
    @Order(2)
    void Should_set_and_compare_dataset() {

    }
}

The project source code used in the current subsection is available at the following link:

9.2.2. One connection per test class

This strategy informs the DBUnit to create a new connection before run all tests for each test class and to close after running all tests for each test class.

@RxMicroIntegrationTest
@Testcontainers
(1)
@DbUnitTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
// FIXME "org.postgresql.util.PSQLException: FATAL: the database system is starting up" and remove @Ignore
@Ignore
final class DbStateUsingPerClassConnectionTest {

    @Container
    private static final GenericContainer<?> POSTGRESQL_TEST_DB =
            new GenericContainer<>("rxmicro/postgres-test-db")
                    .withExposedPorts(5432);

    @BeforeAll
    static void beforeAll() {
        getCurrentTestDatabaseConfig()
                .setHost(POSTGRESQL_TEST_DB.getContainerIpAddress())
                .setPort(POSTGRESQL_TEST_DB.getFirstMappedPort());
    }

    @Test
    @ExpectedDataSet("dataset/rxmicro-test-dataset.xml")
    @Order(1)
    void Should_contain_expected_dataset() {

    }

    @Test
    @InitialDataSet("dataset/rxmicro-test-dataset-two-rows-only.xml")
    @ExpectedDataSet("dataset/rxmicro-test-dataset-two-rows-only.xml")
    @Order(2)
    void Should_set_and_compare_dataset() {

    }
}

The project source code used in the current subsection is available at the following link:

9.2.3. One connection per test method

This strategy informs the DBUnit to create a new connection before each test method and to close after each one.

@RxMicroIntegrationTest
@Testcontainers
(1)
@DbUnitTest(retrieveConnectionStrategy = PER_TEST_METHOD)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
// FIXME "org.postgresql.util.PSQLException: FATAL: the database system is starting up" and remove @Ignore
@Ignore
final class DbStateUsingPerMethodConnectionTest {

    @Container
    private final GenericContainer<?> postgresqlTestDb =
            new GenericContainer<>("rxmicro/postgres-test-db")
                    .withExposedPorts(5432);

    @BeforeEach
    void beforeEach() {
        getCurrentTestDatabaseConfig()
                .setHost(postgresqlTestDb.getContainerIpAddress())
                .setPort(postgresqlTestDb.getFirstMappedPort());
    }

    @Test
    @ExpectedDataSet("dataset/rxmicro-test-dataset.xml")
    @Order(1)
    void Should_contain_expected_dataset() {

    }

    @Test
    @InitialDataSet("dataset/rxmicro-test-dataset-two-rows-only.xml")
    @ExpectedDataSet("dataset/rxmicro-test-dataset-two-rows-only.xml")
    @Order(2)
    void Should_set_and_compare_dataset() {

    }
}

The project source code used in the current subsection is available at the following link:

9.3. @InitialDataSet Annotation

The @InitialDataSet annotation inform the RxMicro framework that it is necessary to prepare the tested database using DbUnit framework before test execution. The @InitialDataSet annotation contains the dataset files that must be exported to the tested database before test execution.

@Test
(1)
@InitialDataSet("dataset/rxmicro-test-dataset-two-rows-only.xml")
void prepare_database() {
    // do something with prepared database
}
1 Using data from the @InitialDataSet annotation the RxMicro framework prepares the tested database.

The init dataset can be provided using flat xml format:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE dataset SYSTEM "rxmicro-test-dataset.dtd">

<dataset>
    <account id="1"
             email="richard.hendricks@piedpiper.com"
             first_name="Richard"
             last_name="Hendricks"
             balance="70000.00"
             role="CEO"/>
    <account id="3"
             email="dinesh.chugtai@piedpiper.com"
             first_name="Dinesh"
             last_name="Chugtai"
             balance="10000.00"
             role="Lead_Engineer"/>

    <product id="24"
             name="Apple iMac 27&quot; with Retina 5K Display Late (MQ2Y2)"
             price="6200.00"
             count="7"/>
    <product id="25"
             name="Apple iMac 27&quot; 2017 5K (MNEA2)"
             price="2100.00"
             count="17"/>

    <order/>
</dataset>

The project source code used in the current subsection is available at the following link:

9.4. @ExpectedDataSet Annotation

The @ExpectedDataSet annotation inform the RxMicro framework that it is necessary to compare the actual database state with the expected one, defined using dataset file(s) after test execution.

@Test
(1)
@ExpectedDataSet("dataset/rxmicro-test-dataset-two-rows-only.xml")
void verify_database_state() {
    // change database state
}
1 Using data from the @ExpectedDataSet annotation the RxMicro framework compares actual database state with the expected one after test execution.

The expected dataset can be provided using flat xml format:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE dataset SYSTEM "rxmicro-test-dataset.dtd">

<dataset>
    <account id="1"
             email="richard.hendricks@piedpiper.com"
             first_name="Richard"
             last_name="Hendricks"
             balance="70000.00"
             role="CEO"/>
    <account id="3"
             email="dinesh.chugtai@piedpiper.com"
             first_name="Dinesh"
             last_name="Chugtai"
             balance="10000.00"
             role="Lead_Engineer"/>

    <product id="24"
             name="Apple iMac 27&quot; with Retina 5K Display Late (MQ2Y2)"
             price="6200.00"
             count="7"/>
    <product id="25"
             name="Apple iMac 27&quot; 2017 5K (MNEA2)"
             price="2100.00"
             count="17"/>

    <order/>
</dataset>

The project source code used in the current subsection is available at the following link:

9.5. @RollbackChanges Annotation

The @RollbackChanges annotation starts a new transaction before initialization of database by the @InitialDataSet annotation (if it is present) and rolls back this transaction after comparing actual database state with expected one provided by the @ExpectedDataSet annotation (if it is present).

The isolation level of the test transaction can be configured using isolationLevel parameter declared at the @RollbackChanges annotation.

@RxMicroIntegrationTest
@Testcontainers
(1)
@DbUnitTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
// FIXME "org.postgresql.util.PSQLException: FATAL: the database system is starting up" and remove @Ignore
@Ignore
final class RollbackChangesTest {

    @Container
    private static final GenericContainer<?> POSTGRESQL_TEST_DB =
            new GenericContainer<>("rxmicro/postgres-test-db")
                    .withExposedPorts(5432);

    @BeforeAll
    static void beforeAll() {
        getCurrentTestDatabaseConfig()
                .setHost(POSTGRESQL_TEST_DB.getContainerIpAddress())
                .setPort(POSTGRESQL_TEST_DB.getFirstMappedPort());
    }

    @Test
    (2)
    @RollbackChanges
    @InitialDataSet("dataset/rxmicro-test-dataset-empty.xml")
    @ExpectedDataSet("dataset/rxmicro-test-dataset-empty.xml")
    @Order(1)
    void Should_set_and_compare_dataset() {

    }

    @Test
    @ExpectedDataSet("dataset/rxmicro-test-dataset.xml")
    @Order(2)
    void Should_contain_expected_dataset() {

    }
}
1 For database testing it is necessary to inform the RxMicro framework that current test is DBUnit test.
2 If test method annotated by the @RollbackChanges annotation all changes made by this test method will be rolled back.

The project source code used in the current subsection is available at the following link:

9.6. Ordered Comparison

If the expected dataset is ordered, it is necessary to inform the RxMicro framework how to compare this dataset correctly. For this case the @ExpectedDataSet annotation has orderBy parameter:

@Test
@ExpectedDataSet(
        value = "dataset/rxmicro-test-dataset-products-order-by-price.xml",
        orderBy = "price" (1)
)
@Order(1)
void Should_contain_expected_dataset() {

}
1 The orderBy parameter contains the column name(s) that must be used to sort the actual dataset before comparison.

The dataset/rxmicro-test-dataset-products-order-by-price.xml dataset is the following:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE dataset SYSTEM "rxmicro-test-dataset.dtd">

<dataset>
    <product id="19" name="Apple iPod Touch 6 16Гб (MKGX2)" price="310.00" count="48"/>
    <product id="17" name="Apple iPod Touch 6 16Гб (MKH62)" price="320.00" count="54"/>
    <product id="18" name="Apple iPod Touch 6 32Гб (MKJ02)" price="380.00" count="52"/>
    <product id="20" name="Apple iPod Touch 6 32Гб (MKHV2)" price="420.00" count="55"/>
    <product id="14" name="Apple iPhone 7 Plus 32GB Black" price="510.00" count="3"/>
    <product id="6" name="Apple iPad (2019) 10.2&quot; Wi-Fi 32GB Gold (MW762)" price="540.00"
             count="32"/>
    <product id="7" name="Apple iPad (2019) 10.2&quot; Wi-Fi 128GB Silver (MW782)" price="620.00"
             count="37"/>
    <product id="8" name="Apple iPad mini 5 Wi-Fi 64Gb Space Gray (MUQW2)" price="645.00"
             count="26"/>
    <product id="12" name="Apple iPhone Xr 64GB Black (MRY42)" price="760.00" count="14"/>
    <product id="10" name="Apple iPhone Xs 64GB Space Gray (MT9E2)" price="840.00" count="21"/>
    <product id="13" name="Apple iPhone Xs 256GB Space Gray (MT9H2)" price="910.00" count="10"/>
    <product id="11" name="Apple iPhone 11 128GB Black" price="980.00" count="18"/>
    <product id="2" name="Apple MacBook A1534 12&quot; Space Gray (MNYF2)" price="985.00"
             count="12"/>
    <product id="9" name="Apple iPad Pro 11&quot; Wi-Fi 64GB Space Gray 2018 (MTXN2)" price="1100.00"
             count="18"/>
    <product id="4" name="Apple MacBook Pro 13 Retina Space Gray (MPXT2) 2017" price="1345.00"
             count="17"/>
    <product id="15" name="Apple iPhone 11 Pro 64GB Space Gray" price="1450.00" count="42"/>
    <product id="16" name="Apple iPhone 11 Pro 256GB Midnight Green" price="1720.00" count="38"/>
    <product id="22" name="Apple iMac 21.5&apos; Middle 2017 (MMQA2)" price="1740.00" count="14"/>
    <product id="5" name="Apple MacBook Pro 15&quot; Retina Z0RF00052 (Mid 2015)" price="1860.00"
             count="11"/>
    <product id="23" name="Apple iMac 21&quot; Retina 4K MRT32 (Early 2019)" price="1920.00"
             count="11"/>
    <product id="25" name="Apple iMac 27&quot; 2017 5K (MNEA2)" price="2100.00" count="17"/>
    <product id="3" name="Apple MacBook Pro 16&quot; 512GB 2019 (MVVJ2) Space Gray" price="2540.00"
             count="8"/>
    <product id="1" name="Apple MacBook Pro 15&quot; Retina Z0WW00024 Space Gray" price="5750.00"
             count="10"/>
    <product id="24" name="Apple iMac 27&quot; with Retina 5K Display Late (MQ2Y2)" price="6200.00"
             count="7"/>
    <product id="21" name="Apple iMac Pro 27&quot; Z0UR000AC / Z0UR8 (Late 2017)" price="7800.00"
             count="6"/>
</dataset>

The project source code used in the current subsection is available at the following link:

9.7. Supported Expressions

The RxMicro framework supports expressions for datasets.
Expressions can be useful to set dynamic parameters.

The RxMicro framework supports the following expressions:

  • ${null} - null value.

  • ${now} - is java.time.Instant.now() value for the initial dataset;

    • ${instant:now} is alias for ${now};

    • ${timestamp:now} is alias for ${now};

  • ${now} - is java.time.Instant.now() value for the expected dataset;

    • ${instant:now} is alias for ${now};

    • ${timestamp:now} is alias for ${now};

    • ${now:${CUSTOM-DURATION}} is alias for ${now};

    • ${instant:now:${CUSTOM-DURATION}} is alias for ${now};

    • ${timestamp:now:${CUSTOM-DURATION}} is alias for ${now};

  • ${interval:${MIN}:${MAX}} - is an instant interval that can be compared with java.time.Instant and java.sql.Timestamp instances correctly.

    • ${interval:${MEDIAN}:${LEFT-DELTA}:${RIGHT-DELTA}} is alias for ${interval:${MIN}:${MAX}};

    • ${instant:interval:${MEDIAN}:${LEFT-DELTA}:${RIGHT-DELTA}} is alias for ${interval:${MIN}:${MAX}};

    • ${timestamp:interval:${MEDIAN}:${LEFT-DELTA}:${RIGHT-DELTA}} is alias for ${interval:${MIN}:${MAX}};

    • ${instant:interval:${MIN}:${MAX}} is alias for ${interval:${MIN}:${MAX}};

    • ${timestamp:interval:${MIN}:${MAX}} is alias for ${interval:${MIN}:${MAX}};

  • ${int:interval:${MIN}:${MAX}} - is an integer number interval that can be compared with java.lang.Short, java.lang.Integer and java.lang.Long instances correctly.

    • ${integer:interval:${MIN}:${MAX}} is alias for ${int:interval:${MIN}:${MAX}};

    • ${tinyint:interval:${MIN}:${MAX}} is alias for ${int:interval:${MIN}:${MAX}};

    • ${short:interval:${MIN}:${MAX}} is alias for ${int:interval:${MIN}:${MAX}};

    • ${smallint:interval:${MIN}:${MAX}} is alias for ${int:interval:${MIN}:${MAX}};

    • ${long:interval:${MIN}:${MAX}} is alias for ${int:interval:${MIN}:${MAX}};

    • ${bigint:interval:${MIN}:${MAX}} is alias for ${int:interval:${MIN}:${MAX}};

9.7.1. ${null} Expression

If dataset must contain null value, the ${null} expression must be used:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE dataset SYSTEM "rxmicro-test-dataset.dtd">

<dataset>
    <account id="1"
             email="richard.hendricks@piedpiper.com"
             first_name="Richard"
             last_name="Hendricks"
             role="CEO"
             balance="${null}"/> (1)
</dataset>
1 Set null value to the balance column.
@Test
@InitialDataSet(
        (1)
        value = "dataset/with-null-expression-dataset.xml",
        (2)
        executeStatementsBefore = "ALTER TABLE account ALTER COLUMN balance DROP NOT NULL")
(4)
@ExpectedDataSet("dataset/with-null-expression-dataset.xml")
void Should_set_and_compare_null_correctly() throws SQLException {
    try (Connection connection = getJDBCConnection()) {
        try (Statement st = connection.createStatement()) {
            try (ResultSet rs = st.executeQuery("SELECT balance FROM account WHERE id=1")) {
                if (rs.next()) {
                    (3)
                    assertNull(rs.getTimestamp(1, GREGORIAN_CALENDAR_WITH_UTC_TIME_ZONE));
                } else {
                    fail("Test database does not contain account with id=1");
                }
            }
        }
    }
}
1 The dataset with ${null} expression.
2 The test database does not support null values for account.balance column. To demonstrate how ${null} expression is worked it is necessary to drop NOT NULL constraint.
3 The actual row contains null value.
4 After test execution the test database must contain null value.

The project source code used in the current subsection is available at the following link:

9.7.2. ${now} Expression

The ${now} expression useful if it is necessary to work with current instant: set and compare.

If dataset must contain java.time.Instant.now() value, the ${now} expression must be used:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE dataset SYSTEM "rxmicro-test-dataset.dtd">

<dataset>
    <order id="1"
           id_account="1"
           id_product="1"
           count="10"
           created="${now}"/> (1)
</dataset>
1 Set current instant value to the created column.
@Test
@InitialDataSet(
        (1)
        value = "dataset/with-now-expression-dataset.xml",
        (2)
        executeStatementsBefore = {
                "ALTER TABLE \"order\" DROP CONSTRAINT IF EXISTS order_fk_account",
                "ALTER TABLE \"order\" DROP CONSTRAINT IF EXISTS order_fk_product"
        })
(3)
@ExpectedDataSet("dataset/with-now-expression-dataset.xml")
void Should_set_and_compare_now_correctly() throws SQLException {
    try (Connection connection = getJDBCConnection()) {
        try (Statement st = connection.createStatement()) {
            try (ResultSet rs = st.executeQuery("SELECT created FROM \"order\" WHERE id=1")) {
                if (rs.next()) {
                    (4)
                    final Instant actual =
                            rs.getTimestamp(1, GREGORIAN_CALENDAR_WITH_UTC_TIME_ZONE).toInstant();
                    (5)
                    assertInstantEquals(Instant.now(), actual);
                } else {
                    fail("Test database does not contain order with id=1");
                }
            }
        }
    }
}
1 The dataset with ${now} expression.
2 The test database contains foreign keys. To demonstrate how ${now} expression is worked it is necessary to drop these foreign keys.
3 The expected dataset must contain current instant value too.
4 The actual row contains current instant value.
5 To compare current instant value it is necessary to use ExAssertions.assertInstantEquals method. This method verifies that instants are equal within the default delta configured via GlobalTestConfig config class.

The ${now} expression can be used to verify that unit test creates java.time.Instant.now() value:

@Test
@InitialDataSet(
        (1)
        value = "dataset/empty-database.xml",
        (2)
        executeStatementsBefore = {
                "ALTER TABLE \"order\" DROP CONSTRAINT IF EXISTS order_fk_account",
                "ALTER TABLE \"order\" DROP CONSTRAINT IF EXISTS order_fk_product"
        })
(4)
@ExpectedDataSet("dataset/with-now-expression-dataset.xml")
void Should_compare_now_correctly() throws SQLException {
    final String sql = "INSERT INTO \"order\" VALUES(?,?,?,?,?)";
    try (Connection connection = getJDBCConnection()) {
        try (PreparedStatement st = connection.prepareStatement(sql)) {
            st.setInt(1, 1);
            st.setInt(2, 1);
            st.setInt(3, 1);
            st.setInt(4, 10);
            final Timestamp now = Timestamp.from(Instant.now()); (3)
            st.setTimestamp(5, now, GREGORIAN_CALENDAR_WITH_UTC_TIME_ZONE);
            st.executeUpdate();
        }
    }
}
1 The empty dataset.
2 The test database contains foreign keys. To demonstrate how ${now} expression is worked it is necessary to drop these foreign keys.
3 The java.time.Instant.now() value is stored to test database.
4 After test execution the test database must contain java.time.Instant.now() value.

The dataset/with-now-expression-dataset.xml classpath resource contains the following content:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE dataset SYSTEM "rxmicro-test-dataset.dtd">

<dataset>
    <order id="1"
           id_account="1"
           id_product="1"
           count="10"
           created="${now}"/> (1)
</dataset>
1 The created column must contain java.time.Instant.now() value.

The project source code used in the current subsection is available at the following link:

9.7.3. ${instant:interval} Expression

The ${instant:interval} expression allows comparing this expression with java.time.Instant and java.sql.Timestamp instances correctly.

This expression is useful if Your business logic generates random instant value within predefined boundaries:

@Test
@InitialDataSet(
        (1)
        value = "dataset/empty-database.xml",
        (2)
        executeStatementsBefore = {
                "ALTER TABLE \"order\" DROP CONSTRAINT IF EXISTS order_fk_account",
                "ALTER TABLE \"order\" DROP CONSTRAINT IF EXISTS order_fk_product"
        })
(4)
@ExpectedDataSet("dataset/with-instant-interval-expression-dataset.xml")
void Should_compare_instant_interval_correctly() throws SQLException {
    final String sql = "INSERT INTO \"order\" VALUES(?,?,?,?,?)";
    try (Connection connection = getJDBCConnection()) {
        try (PreparedStatement st = connection.prepareStatement(sql)) {
            st.setInt(1, 1);
            st.setInt(2, 1);
            st.setInt(3, 1);
            st.setInt(4, 10);
            final Duration duration = Duration.ofSeconds(new Random().nextInt(10) + 1);
            final Timestamp now = Timestamp.from(Instant.now().plus(duration)); (3)
            st.setTimestamp(5, now, GREGORIAN_CALENDAR_WITH_UTC_TIME_ZONE);
            st.executeUpdate();
        }
    }
}
1 The empty dataset.
2 The test database contains foreign keys. To demonstrate how ${now} expression is worked it is necessary to drop these foreign keys.
3 The java.time.Instant.now() + [1 SECOND - 10 SECOND] value is stored to test database.
4 After test execution the test database must contain java.time.Instant.now() + [1 SECOND - 10 SECOND] value.

The dataset/with-now-expression-dataset.xml classpath resource contains the following content:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE dataset SYSTEM "rxmicro-test-dataset.dtd">

<dataset>
    <order id="1"
           id_account="1"
           id_product="1"
           count="10"
           created="${interval:now:PT1S:PT12S}"/> (1)
</dataset>
1 The created column must contain java.time.Instant.now() + [1 SECOND - 12 SECOND] value. 12 instead of 10 is used because 2 second is compare delta!

The project source code used in the current subsection is available at the following link:

9.7.4. ${int:interval} Expression

The ${int:interval} expression allows comparing this expression with java.lang.Short, java.lang.Integer and java.lang.Long instances correctly.

This expression is useful if Your business logic generates random integer number value within predefined boundaries:

@Test
@InitialDataSet("dataset/empty-database.xml")
(2)
@ExpectedDataSet("dataset/with-int-interval-expression-dataset.xml")
void Should_compare_int_interval_correctly() throws SQLException {
    final String sql = "INSERT INTO product VALUES(?,?,?,?)";
    try (Connection connection = getJDBCConnection()) {
        try (PreparedStatement st = connection.prepareStatement(sql)) {
            st.setInt(1, 1);
            st.setString(2, "name");
            st.setBigDecimal(3, new BigDecimal("6200.00"));
            st.setInt(4, new Random().nextInt(10)); (1)
            st.executeUpdate();
        }
    }
}
1 The actual product count is random value from 0 to 9. To compare the actual dataset it is necessary to use ${int:interval} expression.
2 After test execution the test database must contain random integer number value from 0 to 9.

The expected dataset with the integer number interval from 0 to 9:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE dataset SYSTEM "rxmicro-test-dataset.dtd">

<dataset>
    <product id="1"
             name="name"
             price="6200.00"
             count="${int:interval:0:9}"/> (1)
</dataset>
1 Set integer number interval from 0 to 9.

The project source code used in the current subsection is available at the following link:

Java EcoSystem Integration

Home

Appendices