© 2019-2022 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:
-
The
rxmicro.test
is a basic module designed for test writing using any modern testing framework; -
The
rxmicro.test.junit
is a module designed for test writing using the JUnit 5 framework; -
The
rxmicro.test.mockito
is a module designed for test writing using the Mockito framework; -
The
rxmicro.test.mockito.junit
is a module designed for test writing using the JUnit 5 and Mockito frameworks. -
The
rxmicro.test.dbunit
is a module designed for test writing using the DbUnit framework; -
The
rxmicro.test.dbunit.junit
is a module designed for test writing using the DbUnit and JUnit 5 frameworks;
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:
-
Define the versions of used libraries.
-
Add the required dependencies to the
pom.xml
. -
Configure the
maven-compiler-plugin
. -
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.11</rxmicro.version> (1)
<maven-compiler-plugin.version>3.10.1</maven-compiler-plugin.version> (2)
<maven-surefire-plugin.version>3.0.0-M7</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 The
|
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 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:
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 |
Annotation | Description |
---|---|
Declares the test class field as an alternative. The RxMicro framework supports alternatives only for REST-based microservice tests and component unit tests. |
|
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
The RxMicro framework supports test configuration only for REST-based microservice tests and component unit tests. |
|
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 The RxMicro framework supports test configuration only for REST-based microservice tests and component unit tests. |
|
Allows to configure the following component:
(This annotation applies only to the
The RxMicro framework supports the |
|
Declares the test class as a REST-based microservice test. |
|
Declares the test class as a component unit test. |
|
Declares the test class as a REST-based microservice integration test. |
|
Declares the test class as a DBUnit integration test. |
|
It is used to specify the method to be invoked by the RxMicro framework before running the test method. The RxMicro framework supports the |
|
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 |
|
Informs the test framework about the need to create mocks and inject them into the test class fields, annotated by the
(Using the |
|
Provides the init state of tested database before execution of the test method. |
|
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 |
|
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 theRxMicro 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:
-
The alternative instance is created by the developer in the test code or by the testing framework automatically.
-
Once all alternatives have been created, they are registered in the runtime container.
-
Once all alternatives have been registered, the RxMicro framework creates an instance of the tested class.
-
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.
-
Since the runtime container already contains an alternative instead of the real component, the alternative is injected into the instance of the tested class.
-
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:
-
The alternative instance is created by the developer in the test code or by the testing framework automatically.
-
The RxMicro framework creates an instance of the tested class.
-
In the constructor or static section of the tested class, instances of the real custom components are created.
-
After initialization, the instance of the tested class contains references to the real custom components.
-
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.); -
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 |
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
|
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
|
When creating mock alternatives, the |
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
|
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
|
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
|
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:
-
For each tested component, a search for injection candidates is performed.
-
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 theA
type). -
After receiving a map with candidates for injection, the RxMicro framework passes through this map.
-
For each user type, a list of candidates and a list of alternatives is requested.
-
If there is only one alternative and only one candidate for the user type, the RxMicro framework injects this alternative into the candidate field;
-
If more than one alternative and only one candidate is found, the RxMicro framework will throw out an error;
-
If there is more than one candidate and only one alternative, then:
-
The RxMicro framework analyzes the injection candidate field name:
-
if the candidate field name matches the alternative field name, the RxMicro framework injects this alternative;
-
if the candidate field name matches the value of the
name
parameter of the@Alternative
annotation, the RxMicro framework injects this alternative; -
otherwise this candidate will be skipped;
-
-
if no alternative has been injected, the RxMicro framework injects this alternative in all candidate fields.
(This is the behavior that occurs in theGrandParent2Test
test.)
-
-
If there is more than one candidate and more than one alternative, then:
-
The RxMicro framework analyzes the injection candidate field name:
-
if the candidate field name matches the alternative field name, the RxMicro framework injects this alternative;
(In theGrandParent3Test
test, thegrandParentBusinessService
alternative is injected in theGrandParent
component field, because the names of the alternative and component fields are equal.); -
if the candidate field name matches the value of the
name
parameter of the@Alternative
annotation, the RxMicro framework injects this alternative;
(In theGrandParent3Test
test, thebusinessService
alternative is injected in theChild
component field, because thename
parameter of the@Alternative
annotation is equal to thechildBusinessService
. And in theChild
class, the field name with theBusinessService
type is also equal to thechildBusinessService
.) -
otherwise this candidate will be skipped;
-
-
(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 Please note that since the |
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
|
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:
-
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. -
A special blocking HTTP client to execute HTTP requests during testing:
BlockingHttpClient
. -
The
SystemOut
interface for easy console access.
For each microservice test, the RxMicro framework performs the following actions:
-
Before starting all the test methods in the class:
-
checks the test class for compliance with the rules of REST-based microservice testing defined by the RxMicro framework;
-
starts an HTTP server on a random free port;
-
creates an instance of the
BlockingHttpClient
type; -
connects the created
BlockingHttpClient
to the running HTTP server.
-
-
Before starting each test method:
-
if necessary, invokes the methods defined using the
@BeforeThisTest
or@BeforeIterationMethodSource
annotations; -
if necessary, registers the RxMicro component alternatives in the RxMicro container;
-
registers the tested REST-based microservice on the running HTTP server;
-
if necessary, injects the custom component alternatives to the REST-based microservice;
-
injects a reference to the
BlockingHttpClient
instance into the test class; -
if necessary, creates the
System.out
mock, and injects it into the test class.
-
-
After performing each test method:
-
deletes all registered components from the RxMicro container;
-
deletes all registered REST-based microservices on the running HTTP server;
-
if necessary, restores the
System.out
.
-
-
After performing all the tests in the class:
-
clears the resources of the
BlockingHttpClient
component; -
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 |
The project source code used in the current subsection is available at the following link: |
When compiling, the RxMicro framework searches for When changing the |
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 |
But if on the basis of such test we build the source code coverage report, this report will show a low degree of coverage:
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:
-
create a mock alternative to the
HttpClientFactory
RxMicro component; -
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%:
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 When changing the |
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 |
But if on the basis of such test we build the source code coverage report, this report will show a low degree of coverage:
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:
-
create a mock alternative to the
MongoDatabase
RxMicro component; -
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%:
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 When changing the |
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 |
But if on the basis of such test we build the source code coverage report, this report will show a low degree of coverage:
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:
-
create a mock alternative to the
ConnectionPool
RxMicro component; -
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%:
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 When changing the |
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:
-
The
MOCKITO
prefix means that the action is activated by the@InitMocks
annotation. -
The
RX-MICRO
prefix means that the action is activated by the@RxMicroRestBasedMicroServiceTest
annotation. -
The
USER-TEST
prefix means that at this stage a custom method from theMicroServiceTest
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:
-
The
MOCKITO
prefix means that the action is activated by the@InitMocks
annotation. -
The
RX-MICRO
prefix means that the action is activated by the@RxMicroComponentTest
annotation. -
The
USER-TEST
prefix means that at this stage a custom method from theBusinessServiceTest
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:
-
The additional
@RxMicroIntegrationTest
annotation, that informs the RxMicro framework about the need to create an HTTP client and inject it into the tested class. -
A special blocking HTTP client to execute HTTP requests during testing:
BlockingHttpClient
. -
The
SystemOut
interface for easy console access.
The main differences between integration testing and REST-based microservice testing:
-
for integration tests, the RxMicro framework does not run an HTTP server;
-
the developer has to start and stop the system consisting of REST-based microservices;
-
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
Thus, the integration test unlike the REST-based microservice test can only inject a blocking HTTP client and create a mock for the |
To get additional info about writing tests that require JSON object comparison, please read |
The project source code used in the current subsection is available at the following link: |
When compiling, the RxMicro framework searches for When changing the |
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 |
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
|
To get additional info about writing tests that require JSON object comparison, please read |
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.getHost())
.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.getHost())
.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.getHost())
.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" with Retina 5K Display Late (MQ2Y2)"
price="6200.00"
count="7"/>
<product id="25"
name="Apple iMac 27" 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" with Retina 5K Display Late (MQ2Y2)"
price="6200.00"
count="7"/>
<product id="25"
name="Apple iMac 27" 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.getHost())
.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" Wi-Fi 32GB Gold (MW762)" price="540.00"
count="32"/>
<product id="7" name="Apple iPad (2019) 10.2" 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" Space Gray (MNYF2)" price="985.00"
count="12"/>
<product id="9" name="Apple iPad Pro 11" 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' Middle 2017 (MMQA2)" price="1740.00" count="14"/>
<product id="5" name="Apple MacBook Pro 15" Retina Z0RF00052 (Mid 2015)" price="1860.00"
count="11"/>
<product id="23" name="Apple iMac 21" Retina 4K MRT32 (Early 2019)" price="1920.00"
count="11"/>
<product id="25" name="Apple iMac 27" 2017 5K (MNEA2)" price="2100.00" count="17"/>
<product id="3" name="Apple MacBook Pro 16" 512GB 2019 (MVVJ2) Space Gray" price="2540.00"
count="8"/>
<product id="1" name="Apple MacBook Pro 15" Retina Z0WW00024 Space Gray" price="5750.00"
count="10"/>
<product id="24" name="Apple iMac 27" with Retina 5K Display Late (MQ2Y2)" price="6200.00"
count="7"/>
<product id="21" name="Apple iMac Pro 27" 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}
- isjava.time.Instant.now()
value for the initial dataset;-
${instant:now}
is alias for${now}
; -
${timestamp:now}
is alias for${now}
;
-
-
${now}
- isjava.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 withjava.time.Instant
andjava.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 withjava.lang.Short
,java.lang.Integer
andjava.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: |