如何仅在标记测试后 运行 清除方法?

How can I run cleanup method only after tagged tests?

我正在为我的 Java 项目编写 JUnit 5 测试。

我有一些测试方法需要耗时清理(在每个方法之后)。理想情况下,我想用一些注释和 运行 清理方法来标记它们。

这是我试过的:

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
    }
}

我希望 cleanup 方法仅在 test2 之后成为 运行。但实际上在两次测试后 运行s。

是否可以通过 JUnit 5 注释的某种组合来实现它?我不想将我的测试 class 分成几个 class 或直接从测试方法调用 cleanup

您可以将 TestInfo 注入到测试中并检查测试用哪些标签注释:

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
  }

}

您可以创建自己的 AfterEachCallback 扩展并将其应用于所需的测试方法。此扩展将在应用它的每个测试后执行。然后,使用自定义注释,您可以 link 具有特定测试的特定清理方法。这是扩展的示例:

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 "";

  }

}

然后你的测试 class 可能看起来像:

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()");
  }

}

运行 Tests 我得到以下输出:

#testWithExtension()
#performCleanup()
#testWithExtension_2()
#performCleanup_2()
#testWithoutExtension()

此扩展将应用于任何用 CleanupExtension.CleanupExtendWith(CleanupExtension.class) 注释的测试方法。前一个注释的目的是将配置与也应用扩展的注释结合起来。然后,在每个测试方法之后,扩展将调用 class 层次结构中用 CleanupExtension.CleanMethod 注释的任何方法。 CleanupCleanMethod 都有一个 String 属性。此属性是 "name",只有 CleanMethodCleanup 测试具有匹配的 "name" 才会被执行。这允许您 link 特定的测试方法到特定的清理方法。


有关 JUnit Jupiter 扩展的更多信息,请参阅 §5 of the User Guide. Also, for CleanupExtension.Cleanup I'm using the Meta-Annotation/Composed-Annotation feature described in §3.1.1

请注意,这比 @Roman Konoval 给出的 更复杂,但如果您必须多次执行此类操作,它可能对用户更友好。但是,如果您只需要为一两个测试执行此操作 classes 我推荐 Roman 的答案。

来自文档:

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.

关于上面的描述,你可以使用 TestInfo class 它给你信息 class cleanUp 应该是 运行 ,然后你需要检查条件并允许那些你想要的通过检查他们的标签:

@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() {

}