问题
I'm writing JUnit 5 tests for my Java project.
I have some test methods that require time consuming clean up (after each of them). Ideally, I would like to mark them with some annotation and run cleanup method only for them.
This is what I tried:
class MyTest {
@AfterEach
@Tag("needs-cleanup")
void cleanup() {
//do some complex stuff
}
@Test
void test1() {
//do test1
}
@Test
@Tag("needs-cleanup")
void test2() {
//do test2
}
}
I want cleanup
method to be run only after test2
. But it actually runs after both tests.
Is it possible to achieve it via some combination of JUnit 5 annotations? I don't want to split my test class into several classes or call cleanup
from test methods directly.
回答1:
From Documentation:
TestInfo: if a method parameter is of type TestInfo, the TestInfoParameterResolver will supply an instance of TestInfo corresponding to the current test as the value for the parameter. The TestInfo can then be used to retrieve information about the current test such as the test’s display name, the test class, the test method, or associated tags. The display name is either a technical name, such as the name of the test class or test method, or a custom name configured via @DisplayName.
TestInfo acts as a drop-in replacement for the TestName rule from JUnit 4.
Regarding above description, you can use TestInfo class which gives you information of the class that cleanUp is supposed to be run for,then you need check the condition and allow those you want by checking their tags:
@AfterEach
void afterEach(TestInfo info) {
if(!info.getTags().contains("cleanItUp")) return; // preconditioning only to needs clean up
//// Clean up logic Here
}
@Test
@Tag("cleanItUp")
void myTest() {
}
回答2:
You can inject test into the test and check what tags the test is annotated with:
class MyTest {
private TestInfo testInfo;
MyTest(TestInfo testInfo) {
this.testInfo = testInfo;
}
@AfterEach
void cleanup() {
if (this.testInfo.getTags().contains("needs-cleanup")) {
// .. do cleanup
}
}
@Test
void test1() {
//do test1
}
@Test
@Tag("needs-cleanup")
void test2() {
//do test2
}
}
回答3:
You can create your own AfterEachCallback extension and apply it to the needed test methods. This extension will execute after every test it's applied to. Then, using custom annotations, you can link specific cleanup methods with specific tests. Here's an example of the extension:
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.List;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.commons.support.HierarchyTraversalMode;
public class CleanupExtension implements AfterEachCallback {
private static final Namespace NAMESPACE = Namespace.create(CleanupExtension.class);
private static boolean namesMatch(Method method, String name) {
return method.getAnnotation(CleanMethod.class).value().equals(name);
}
private static Exception suppressOrReturn(final Exception previouslyThrown,
final Exception newlyThrown) {
if (previouslyThrown == null) {
return newlyThrown;
}
previouslyThrown.addSuppressed(newlyThrown);
return previouslyThrown;
}
@Override
public void afterEach(final ExtensionContext context) throws Exception {
final Method testMethod = context.getRequiredTestMethod();
final Cleanup cleanupAnno = testMethod.getAnnotation(Cleanup.class);
final String cleanupName = cleanupAnno == null ? "" : cleanupAnno.value();
final List<Method> cleanMethods = getAnnotatedMethods(context);
final Object testInstance = context.getRequiredTestInstance();
Exception exception = null;
for (final Method method : cleanMethods) {
if (namesMatch(method, cleanupName)) {
try {
method.invoke(testInstance);
} catch (Exception ex) {
exception = suppressOrReturn(exception, ex);
}
}
}
if (exception != null) {
throw exception;
}
}
@SuppressWarnings("unchecked")
private List<Method> getAnnotatedMethods(final ExtensionContext methodContext) {
// Use parent (Class) context so methods are cached between tests if needed
final Store store = methodContext.getParent().orElseThrow().getStore(NAMESPACE);
return store.getOrComputeIfAbsent(
methodContext.getRequiredTestClass(),
this::findAnnotatedMethods,
List.class
);
}
private List<Method> findAnnotatedMethods(final Class<?> testClass) {
final List<Method> cleanMethods = AnnotationSupport.findAnnotatedMethods(testClass,
CleanMethod.class, HierarchyTraversalMode.TOP_DOWN);
for (final Method method : cleanMethods) {
if (method.getParameterCount() != 0) {
throw new IllegalStateException("Methods annotated with "
+ CleanMethod.class.getName() + " must not have parameters: "
+ method
);
}
}
return cleanMethods;
}
@ExtendWith(CleanupExtension.class)
@Retention(RUNTIME)
@Target(METHOD)
public @interface Cleanup {
String value() default "";
}
@Retention(RUNTIME)
@Target(METHOD)
public @interface CleanMethod {
String value() default "";
}
}
And then your test class could look like:
import org.junit.jupiter.api.Test;
class Tests {
@Test
@CleanupExtension.Cleanup
void testWithExtension() {
System.out.println("#testWithExtension()");
}
@Test
void testWithoutExtension() {
System.out.println("#testWithoutExtension()");
}
@Test
@CleanupExtension.Cleanup("alternate")
void testWithExtension_2() {
System.out.println("#testWithExtension_2()");
}
@CleanupExtension.CleanMethod
void performCleanup() {
System.out.println("#performCleanup()");
}
@CleanupExtension.CleanMethod("alternate")
void performCleanup_2() {
System.out.println("#performCleanup_2()");
}
}
Running Tests
I get the following output:
#testWithExtension()
#performCleanup()
#testWithExtension_2()
#performCleanup_2()
#testWithoutExtension()
This extension will be applied to any test method annotated with CleanupExtension.Cleanup
or ExtendWith(CleanupExtension.class)
. The purpose of the former annotation is to combine configuration with an annotation that also applies the extension. Then, after each test method the extension will invoke any methods in the class hierarchy that is annotated with CleanupExtension.CleanMethod
. Both Cleanup
and CleanMethod
have a String
attribute. This attribute is the "name" and only CleanMethod
s that have a matching "name" to the Cleanup
test will be executed. This allows you to link specific test methods to specific cleanup methods.
For more information on JUnit Jupiter extensions see §5 of the User Guide. Also, for CleanupExtension.Cleanup
I'm using the Meta-Annotation/Composed-Annotation feature described in §3.1.1.
Note this is more complicated than the answer given by @Roman Konoval but it may be more user friendly if you have to do this sort of thing many times. However, if you only need to do this for one or two test classes I recommend Roman's answer.
来源:https://stackoverflow.com/questions/52194480/how-can-i-run-cleanup-method-only-after-tagged-tests