JUnit5
Module provides an Extension
for JUnit5 that allows you to easily test your application.
The concept of the JUnit 5 Kora extension is to test the source code that will eventually be used in production. This implies that dependency container of the main application is involved in the test, it can be limited or its parts can be replaced by stubs if the test requires.
Module allows you to conduct:
Component tests
- testing of a single componentInter-component tests
- testing of several components and their interaction with each otherIntegration tests
- testing of components and interaction with external systems.
It is recommended to additionally test the service artifact packaged in the final image, as black box using TestContainers library.
Dependency¶
Dependency build.gradle
:
Setup JUnit platform build.gradle
:
Dependency build.gradle.kts
:
Setup JUnit platform build.gradle.kts
:
Usage¶
Examples will be shown relative to such an application:
Test¶
The @KoraAppTest
annotation is supposed to be used to annotate the test class.
Parameters of the @KoraAppTest
annotation:
value
- required parameter that points to the class annotated by@KoraApp
, representing a graph of all dependencies that will be available within the test.components
- list of components to be initialized within the test, components that are not declared within the test are specified using special annotation@TestComponent
.modules
- list of modules with components connected in the application, which should be additionally included in the dependency container within the test.
Component¶
In order to use components within a test, it is suggested to use the @TestComponent
annotation
which allows injecting component dependencies into arguments and/or fields of the test class.
All components listed in the test fields and/or method/constructor arguments annotated @TestComponent
will be injected as dependencies within the test.
Entire dependency container will be limited to just those components and their dependencies within the test.
It is important that components within the test must be used by at least one @Root component that is also specified within the test.
An example of a test where components are injected in fields:
Example of a test where components are injected in a constructor:
Example of a test where components are injected in method arguments:
Tag¶
In order to inject a dependency/mock that has an @Tag
, you must specify the appropriate @Tag
annotation next to the argument for injection:
Mock¶
It is proposed to use annotations provided by the Mockito library together with the @TestComponent
annotation to create component mock in Java as part of a test.
It is required to add the Mockito library as a build.gradle
dependency:
@Mock and @Spy annotations and all parameters of these annotations are supported. It is recommended to read more about how these annotations work in the official Mockito library documentation.
The @Mock annotation allows you to make a class stub of a
annotated component and control the behavior of its methods with Mockito
or the methods will return default values: void
, default values for primitives, empty collections and null
for all other objects.
The stub component will be injected as a dependency into the arguments and/or fields of the test class and into all components that required it as a dependency. All dependent components that are not required anywhere else within the test will be excluded for non-necessity.
Example of a test using a @Mock
component and injecting a mock in a field:
@KoraAppTest(Application.class)
class SomeTests {
@Mock
@TestComponent
private Supplier<String> component1;
@BeforeEach
void mock() {
Mockito.when(component1.get()).thenReturn("?");
}
@Test
void example() {
assertEquals("?", component1.get());
}
}
@Spy annotation allows you to make a spy facade of a class implementation of a of a component from a dependency container that will have the original behavior of the component's methods by default, but as with stubs, their behavior can be overridden.
The spy component will be implemented as a dependency in the arguments and/or fields of the test class and in all components that required it as a dependency.
Example of a test using @Spy
component and injecting the spy in a method argument:
@KoraAppTest(Application.class)
class SomeTests {
@Test
void example(@MSpy @TestComponent Supplier<String> component1) {
Mockito.when(component1.get()).thenReturn("?");
assertEquals("?", component1.get());
}
}
You can also make a spy from the value of a test class field.
The spy component will be injected as a dependency in the arguments and/or fields of the test class and in all components that required it as a dependency. All dependent components that are not required anywhere else within the test will be excluded for non-necessity.
Example of a test using @Spy
spy component:
In order to mock components in Kotlin, it is suggested to use the annotations provided by the MockK library together with the @TestComponent
annotation.
The MockK library is required to be attached as a build.gradle.kts
dependency:
@MockK and @SpyK annotations and all parameters of these annotations are supported.
It is also possible to use Mockito if desired. For a more detailed description of how Kora and Mockito work, you should read the Java tab of this paragraph. In order to improve the interaction between Mockito and Kotlin you can use the Mockito Kotlin library.
@MockK annotation allows you to make a class mock
annotated component and control the behavior of its methods using MockK
.
Mock component will be injected as a dependency into the arguments and/or fields of the test class and into all components that required it as a dependency. All dependent components that are not required anywhere else within the test will be excluded for non-necessity.
Example of a test using @MockK
component and injecting a mock:
@KoraAppTest(Application::class)
class SomeTests(@MockK @TestComponent val component1: Supplier<String>) {
@BeforeEach
fun mock() {
every { component1.get() } returns "?"
}
@Test
fun example() {
assertEquals("?", component1.get())
}
}
@SpyK annotation allows you to make a spy facade of a class implementation of a of a component from a dependency container that will have the original behavior of the component's methods by default, but as with stubs, their behavior can be overridden.
The spy component will be implemented as a dependency in the arguments and/or fields of the test class and in all components that required it as a dependency.
Example of a test using @SpyK
component and embedding the spy in a method argument:
@KoraAppTest(Application::class)
class SomeTests {
@Test
fun example(@SpyK @TestComponent component1: Supplier<String>) {
every { component1.get() } returns "?"
assertEquals("?", component1.get())
}
}
You can also make a spy from the value of a test class field.
The spy component will be implemented as a dependency in the arguments and/or fields of the test class and in all components that required it as a dependency. All dependent components that are not required anywhere else within the test will be excluded for non-necessity.
An example of a test using the @SpyK
spy component:
Test graph¶
Sometimes you may need to use an extended dependency container as part of your tests. For example, a test container is an application that extends the main application and adds some components from common modules that are not used in this application.
For example, when you have different Read API and Write API applications with common components, which may be required as part of testing one and the other. Or, you may need some save/delete/update functions just for testing as a quick test utility.
????+ warning “Recommendation”
**Highly Recommend Testing** applications as a [black box](https://github.com/kora-projects/kora-examples/blob/master/kora-java-crud/src/test/java/ru/tinkoff/kora/example/crud/BlackBoxTests.java)
and rely on this approach as the primary source of truth and correctness of the application.
Application may work differently depending on the JVM flags,
base image and native libraries, differences between partial and full configurations,
differences in conversion at application entry points, use of schema registries, and so on.
Only a prod-ready image can guarantee the closest possible testing environment.
Let's imagine that the application looks like this:
In tests, you can create a graph extending the main application and use it within tests.
===! “ Java
”
In order to do this, first of all you need to enable the option
to create a sub-module of the main application in `build.gradle`:
```groovy
compileJava {
options.compilerArgs += [
"-Akora.app.submodule.enabled=true"
]
}
```
Then it is required to create an extended test graph of the application:
It is required to exclude scanning of Kora created classes by JUnit (sometimes an error occurs during test search):
You can now use the extended application graph in your tests:
Test configuration¶
By default, the basic configuration will be used, as in the case of running a real application.
For configuration changes/additions within tests, it is assumed that the test class implements the KoraAppTestConfigModifier
interface,
where it is required to implement the KoraConfigModification
method of providing config modification.
It is forbidden to use KoraAppTestConfigModifier
and implementation in the constructor, because in this case it is impossible to get the configuration before implementation.
Environment variables¶
In case the test needs to use the default configuration that would be used when the application is running,
and you only need to substitute environment variables, you can use the SystemProperty
mechanism in KoraConfigModification
:
Suppose there is such a configuration application.conf
:
In order to use such a config and pass only environment variables, you need to return such KoraConfigModification
:
@KoraAppTest(Application.class)
class SomeTests implements KoraAppTestConfigModifier {
@NotNull
@Override
public KoraConfigModification config() {
return KoraConfigModification
.ofSystemProperty("POSTGRES_JDBC_URL", "jdbc:postgresql://localhost:5432/postgres")
.withSystemProperty("POSTGRES_USER", "postgres")
.withSystemProperty("POSTGRES_PASS", "postgres");
}
}
@KoraAppTest(Application::class)
class SomeTests : KoraAppTestConfigModifier {
override fun config(): KoraConfigModification {
return KoraConfigModification
.ofSystemProperty("POSTGRES_JDBC_URL", "jdbc:postgresql://localhost:5432/postgres")
.withSystemProperty("POSTGRES_USER", "postgres")
.withSystemProperty("POSTGRES_PASS", "postgres")
}
}
Configuration file¶
An example of providing a configuration as a file:
Configuration text¶
An example of adding a configuration as a string would look like this, in this case only this configuration will be used without any configuration files:
Container modification¶
In order to add/replace/mock components within an unannotated application dependency container requires implementing the KoraAppTestGraphModifier
interface and
Implement a method to provide a dependency container modifier.
It is forbidden to use KoraAppTestGraphModifier
and embedding in the constructor because then you cannot get the graph before embedding.
Adding¶
An example of adding a component to a graph:
@KoraAppTest(value = Application.class)
class SomeTests implements KoraAppTestGraphModifier {
@Override
public @Nonnull KoraGraphModification graph() {
return KoraGraphModification.create()
.addComponent(TypeRef.of(Supplier.class, Integer.class), () -> (Supplier<Integer>) () -> 1);
}
@Test
void example(@TestComponent Supplier<Integer> supplier) {
assertEquals(1, supplier.get());
}
}
@KoraAppTest(value = Application::class)
class SomeTests : KoraAppTestGraphModifier {
override fun graph(): KoraGraphModification {
return KoraGraphModification.create()
.addComponent(TypeRef.of(Supplier::class.java, Int::class.java), Supplier { Supplier { 1 } })
}
@Test
fun example(@TestComponent supplier: Supplier<Int>) {
assertEquals(1, supplier.get())
}
}
In case it is required to add components using a real component from the graph, this is also available through another method signature:
@KoraAppTest(value = Application.class)
class SomeTests implements KoraAppTestGraphModifier {
@Override
public @Nonnull KoraGraphModification graph() {
return KoraGraphModification.create()
.addComponent(TypeRef.of(Supplier.class, String.class),
(graph) -> {
final Supplier<Integer> existingComponent = (Supplier<Integer>) graph.getFirst(TypeRef.of(Supplier.class, Integer.class));
return (Supplier<String>) () -> "1" + existingComponent.get();
});
}
@Test
void example(@TestComponent Supplier<String> supplier) {
assertEquals(1, supplier.get());
}
}
@KoraAppTest(value = Application::class)
class SomeTests : KoraAppTestGraphModifier {
@Nonnull
override fun graph(): KoraGraphModification {
return KoraGraphModification.create()
.addComponent(TypeRef.of(Supplier::class.java, String::class.java))
{ graph ->
val existingComponent = graph.getFirst(TypeRef.of(Supplier::class.java, Int::class.java))
as Supplier<Int>
Supplier { "1" + existingComponent.get() }
}
}
@Test
fun example(@TestComponent supplier: Supplier<String>) {
assertEquals(1, supplier.get())
}
}
Replacement¶
An example of replacing a component in a dependency container:
@KoraAppTest(value = Application.class)
class SomeTests implements KoraAppTestGraphModifier {
@Override
public @Nonnull KoraGraphModification graph() {
return KoraGraphModification.create()
.replaceComponent(TypeRef.of(Supplier.class, String.class), List.of(Supplier.class), () -> (Supplier<String>) () -> "?");
}
@Test
void example(@Tag(Supplier.class) @TestComponent Supplier<String> supplier) {
assertEquals("?", supplier.get());
}
}
@KoraAppTest(value = Application::class)
class SomeTests : KoraAppTestGraphModifier {
override fun graph(): KoraGraphModification {
return KoraGraphModification.create()
.replaceComponent(TypeRef.of(Supplier::class.java, String::class.java), listOf(Supplier::class.java), Supplier { Supplier { "?" } })
}
@Test
fun example(@Tag(Supplier::class) @TestComponent supplier: Supplier<String>) {
assertEquals("?", supplier.get())
}
}
In case it is required to add components using a real component from the graph, this is also available through another method signature:
@KoraAppTest(value = Application.class)
class SomeTests implements KoraAppTestGraphModifier {
@Override
public @Nonnull KoraGraphModification graph() {
return KoraGraphModification.create()
.replaceComponent(TypeRef.of(Supplier.class, Integer.class),
(graph) -> {
final Supplier<Integer> existingComponent = (Supplier<Integer>) graph.getFirst(TypeRef.of(Supplier.class, Integer.class));
return (Supplier<Integer>) () -> 1 + existingComponent.get();
});
}
@Test
void example(@TestComponent Supplier<Integer> supplier) {
assertEquals(1, supplier.get());
}
}
@KoraAppTest(value = Application::class)
class SomeTests : KoraAppTestGraphModifier {
@Nonnull
override fun graph(): KoraGraphModification {
return KoraGraphModification.create()
.replaceComponent(TypeRef.of(Supplier::class.java, Int::class.java))
{ graph ->
val existingComponent = graph.getFirst(TypeRef.of(Supplier::class.java, Int::class.java))
as Supplier<Int>
Supplier { 1 + existingComponent.get() }
}
}
@Test
fun example(@TestComponent supplier: Supplier<Int>) {
assertEquals(1, supplier.get())
}
}
Initialization¶
In case you want to initialize the dependency container once within the entire test class, you should annotate the test class with @TestInstance(TestInstance.Lifecycle.PER_CLASS)
:
The default behavior is to initialize the container every time of every test method.