JavaLessons/springboot2-junit5-skiptest/README.md

289 lines
9.0 KiB
Markdown
Raw Permalink Normal View History

2019-07-28 22:28:53 +03:00
# SpringBoot JUnit5 SkipTest
* SpringBoot 2
* JUnit 5
# Prerequisites
Imagine the situation that your project must be built on several environments.
Imagine that all tests you've implemented must not be run on each environment.
And you prefer to select which of them should be run by setting it up with... `application.properties` file with concrete property per test.
Looks like delicious, doesn't it?
# Settings
First of all let's disable JUnit 4 supplied in SpringBoot2 by default and enable [JUnit 5](https://junit.org/junit5/docs/current/user-guide/#overview).
Changes in `pom.xml` are:
```xml
<dependencies>
<!--...-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.3.2</version>
2019-07-29 21:04:21 +03:00
<scope>test</scope>
2019-07-28 22:28:53 +03:00
</dependency>
<!--...-->
</dependencies>
```
# Solution
We'd like to annotate each test with simple annotation and point on application property to check if it is `true` to start the test.
## Annotation
Here is our [annotation](src/test/java/com/bvn13/example/springboot/junit/skiptest/TestEnabled.java):
```java
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(TestEnabledCondition.class)
public @interface TestEnabled {
String property();
}
```
## Annotation processor
But this annotation is nothing without its processor.
```java
public class TestEnabledCondition implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
Optional<TestEnabled> annotation = context.getElement().map(e -> e.getAnnotation(TestEnabled.class));
return context.getElement()
.map(e -> e.getAnnotation(TestEnabled.class))
.map(annotation -> {
String property = annotation.property();
return Optional.ofNullable(environment.getProperty(property, Boolean.class))
.map(value -> {
if (Boolean.TRUE.equals(value)) {
return ConditionEvaluationResult.enabled("Enabled by property: "+property);
} else {
return ConditionEvaluationResult.disabled("Disabled by property: "+property);
}
}).orElse(
ConditionEvaluationResult.disabled("Disabled - property <"+property+"> not set!")
);
}).orElse(
ConditionEvaluationResult.enabled("Enabled by default")
);
2019-07-28 22:28:53 +03:00
}
}
```
You must create a class (without Spring @Component annotation) which implements ExecutionCondition interface.
Then you must implement one method of this interface - `ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context)`.
This method takes JUnit test's execution context and returns the condition - should the test be started or not. Simply, right?
You can read more about [Conditional test execution with JUnit5](https://junit.org/junit5/docs/current/user-guide/#extensions-conditions) in official documentation as well.
But how to check application property in this context?
## Obtaining the access to SpringBoot context from JUnit context
Here is the snippet to obtain Spring environment right from the ExtensionContext of JUnit:
```java
Environment environment = SpringExtension.getApplicationContext(context).getEnvironment();
```
2019-07-30 12:26:34 +03:00
Take a look at [full class code of TestEnabledCondition](https://github.com/bvn13/JavaLessons/blob/9a34719dbc7b616f0234e4dcd0d5376905aacc2e/springboot2-junit5-skiptest/src/test/java/com/bvn13/example/springboot/junit/skiptest/TestEnabledCondition.java)
2019-07-28 22:28:53 +03:00
# Make some tests
It's showtime!
Let's create our tests and manage them starts:
```java
@SpringBootTest
public class SkiptestApplicationTests {
@TestEnabled(property = "app.skip.test.first")
@Test
public void testFirst() {
assertTrue(true);
}
@TestEnabled(property = "app.skip.test.second")
@Test
public void testSecond() {
assertTrue(false);
}
}
```
Our `application.propertis` file is look like:
```properties
app.skip.test.first=true
app.skip.test.second=false
```
So...
The result:
![](img/result.png)
2019-07-30 12:26:34 +03:00
# Next step - generalizing properties' names
It is so annoyingly to write the full path to our application properties in every test.
So the next step is to generalify that path in test class annotation.
Let's create a new [annotation](src/test/java/com/bvn13/example/springboot/junit/skiptest/TestEnabledPrefix.java) called `TestEnabledPrefix`:
``` java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestEnabledPrefix {
String prefix();
}
```
## TestEnabledPrefix annotation usage
There is no way avoiding new annotation processing:
### Let's create Annotation Descriptor as follows
```java
public class TestEnabledCondition implements ExecutionCondition {
static class AnnotationDescription {
String name;
Boolean annotationEnabled;
AnnotationDescription(String prefix, String property) {
this.name = prefix + property;
}
String getName() {
return name;
}
AnnotationDescription setAnnotationEnabled(Boolean value) {
this.annotationEnabled = value;
return this;
}
Boolean isAnnotationEnabled() {
return annotationEnabled;
}
}
/* ... */
}
```
It helps us to process annotations using lambdas.
### Then create a method to extract prefix from context
```java
public class TestEnabledCondition implements ExecutionCondition {
/* ... */
private AnnotationDescription makeDescription(ExtensionContext context, String property) {
String prefix = context.getTestClass()
.map(cl -> cl.getAnnotation(TestEnabledPrefix.class))
.map(TestEnabledPrefix::prefix)
.map(pref -> !pref.isEmpty() && !pref.endsWith(".") ? pref + "." : "")
.orElse("");
return new AnnotationDescription(prefix, property);
}
/* ... */
}
```
### And now process the annotation value
2019-07-30 12:26:34 +03:00
``` java
public class TestEnabledCondition implements ExecutionCondition {
/* ... */
2019-07-30 12:26:34 +03:00
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
Environment environment = SpringExtension.getApplicationContext(context).getEnvironment();
return context.getElement()
.map(e -> e.getAnnotation(TestEnabled.class))
.map(TestEnabled::property)
.map(property -> makeDescription(context, property))
.map(description -> description.setAnnotationEnabled(environment.getProperty(description.getName(), Boolean.class)))
.map(description -> {
if (description.isAnnotationEnabled()) {
return ConditionEvaluationResult.enabled("Enabled by property: "+description.getName());
} else {
return ConditionEvaluationResult.disabled("Disabled by property: "+description.getName());
}
}).orElse(
ConditionEvaluationResult.enabled("Enabled by default")
);
2019-07-30 12:26:34 +03:00
}
}
```
##
2019-07-30 12:31:30 +03:00
You can take a look at [full class code](src/test/java/com/bvn13/example/springboot/junit/skiptest/TestEnabledCondition.java) folowing to link.
2019-07-30 12:26:34 +03:00
## New annotation usage
And now we'll apply new annotation to our [test class](src/test/java/com/bvn13/example/springboot/junit/skiptest/SkiptestApplicationTests.java):
2019-07-30 12:26:34 +03:00
``` java
@SpringBootTest
@TestEnabledPrefix(property = "app.skip.test")
public class SkiptestApplicationTests {
@TestEnabled(property = "first")
@Test
public void testFirst() {
assertTrue(true);
}
@TestEnabled(property = "second")
@Test
public void testSecond() {
assertTrue(false);
}
}
```
Much more clear and obvious code.
## Thanks to...
1) Reddit user [dpash](https://www.reddit.com/user/dpash/) for [advice](https://www.reddit.com/r/java/comments/cuiqxf/skip_junit_test_according_to_java_springframework/exxz1yr?utm_source=share&utm_medium=web2x)
2) Reddit user [BoyRobot777](https://www.reddit.com/user/BoyRobot777/) for [advice](https://www.reddit.com/r/java/comments/cuiqxf/skip_junit_test_according_to_java_springframework/exy8cnh?utm_source=share&utm_medium=web2x)