How can I run cleanup method only after tagged tests?

有些话、适合烂在心里 提交于 2019-12-22 18:10:26

问题


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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!