0
@SpringBootTest
@AutoConfigureMockMvc
@ExcludeTags({"no"})
public class MyClassTest {
   @Test
   public void test1() {
   }

   @Test
   @Tag("no")
   public void test2() {
   }
   ...
}

@RunWith(JUnitPlatform.class)
@SelectClasses({MyClassTest.class})
@IncludeTags({"no"})
public class MyClassTestSuiteTest {
}

Having a Spring Boot 2.3.1 project and testing some REST controllers, in a test class some of the test methods are tagged, and shall not be run, when MyClassTest is run. The annotated methods are run in a test suite (with @IncludeTags("no"). JUnit 5.6.2.

With the test suite I'm not sure it @RunWith has to be used for a test suite, or the JUnit 5 @ExtendWith is the right one? In fact, if not necessary, I don't want to mix JUnit 4 and 5, stick to JUnit 5.

Is there a way to configure simply via annotation or similar, to not run the tagged methods when MyClassTest is run? Like @ExcludeTags for test suites, but this does not work on a class like in the example.

Perhaps two test suites can be created, one with @ExludeTags("no"), one with @IncludeTags("no"). But still, how to prevent then that MyClassTest it run at all?

I don't want to create some Run Configuration in a particular IDE. The preferred way would be to use annotations or similar. Perhaps a Maven configuration would also suffice.

Perhaps on test method level execution of the particular test method can be avoided with some criteria evaluation, if the executed test class is MyClassTest, then don't run that test method.

Interesting here is, I cannot replace @RunWith(JUnitPlatform.class) simply with @ExtendWith(JUnitPlatform.class) as there is type incompatibility. Using @ExtendWith(SpringExtension.class) doesn't give me the possibility to run the class (for example with right-click on the class name, no entry to Run/Debug). But @ExtendWith replaces @RunWith in JUnit 5, what extension to use to run the test suite?

neblaz
  • 683
  • 10
  • 32
  • 2
    Why don't use `@Ignore`? – Valijon Jun 16 '20 at 07:20
  • @Ignore on which level? Class or method? If I add it on a method, additionaly I have to remove '@Test', otherwise it is still executed. – neblaz Jun 16 '20 at 07:40
  • You don't need to remove `@Test` annotation. You can use `@Ignore` both on class (it will ignore entire class tests) or methods (only ignore those methods) – Valijon Jun 16 '20 at 07:44
  • Adding '@Ignore' on class shows two differen executions, one for JUnit Vintage that the test class is not run, one for JUnit Jupiter where it is still run. Adding @Ignore on the test method, still runs the test method. Are there different @Ignore? Also with '@Ignore' on the test method, the test method is then not run in the test suite. – neblaz Jun 16 '20 at 07:52
  • Try to use `@Disabled` (Since JUnit 5 it's obsolete). @Ignore refer to `org.junit.Ignore` [link](https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4-ignore-annotation-support) – Valijon Jun 16 '20 at 07:55
  • Also the Junit 5 documentation says: '@Ignore' no longer exists: use '@Disabled'. Btw, how do you mark/highlight code in comments? – neblaz Jun 16 '20 at 07:56
  • @Disabled prevents MyClassTest to be run. But also prevents the tagged test methods in MyClassTest to be run in the test suite MyClassTestSuiteTest. – neblaz Jun 16 '20 at 07:58
  • https://stackoverflow.com/editing-help#comment-formatting – Valijon Jun 16 '20 at 08:04
  • '@Disable' on the class prevents any test method in the class to be run, which is ok. But then the tagged methods are also not run in the test suite. So using '@Disabled' on the class is not an option. Using '@Disabled' on the annotated method prevents the method to be run when MyClassTest is run along with the unannotated methods, but the tagged methods are also not run in the test suite, so using '@Disabled' on tagged test methods also not an option. – neblaz Jun 16 '20 at 08:13

1 Answers1

1

Create Execution Condition ExcludeTagsCondition

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.util.AnnotationUtils;

public class ExcludeTagsCondition implements ExecutionCondition {

    private static final ConditionEvaluationResult ENABLED_IF_EXCLUDE_TAG_IS_INVALID =
            ConditionEvaluationResult.enabled(
                    "@ExcludeTags does not have a valid tag to exclude, all tests will be run");
    private static Set<String> tagsThatMustBeIncluded = new HashSet<>();

    public static void setMustIncludeTags(final Set<String> tagsThatMustBeIncluded) {
        ExcludeTagsCondition.tagsThatMustBeIncluded = new HashSet<>(tagsThatMustBeIncluded);
    }

    @Override
    public ConditionEvaluationResult evaluateExecutionCondition(
            ExtensionContext context) {
        final AnnotatedElement element = context
                .getElement()
                .orElseThrow(IllegalStateException::new);
        final Optional<Set<String>> tagsToExclude = AnnotationUtils.findAnnotation(
                context.getRequiredTestClass(),
                ExcludeTags.class
        )
        .map(a -> 
            Arrays.asList(a.value())
                    .stream()
                    .filter(t -> !tagsThatMustBeIncluded.contains(t))
                    .collect(Collectors.toSet())
        );
        if (!tagsToExclude.isPresent() || tagsToExclude.get().stream()
                .allMatch(s -> (s == null) || s.trim().isEmpty())) {
            return ENABLED_IF_EXCLUDE_TAG_IS_INVALID;
        }
        final Optional<String> tag = AnnotationUtils.findAnnotation(element, Tag.class)
                .map(Tag::value);
        if (tagsToExclude.get().contains(tag.map(String::trim).orElse(""))) {
            return ConditionEvaluationResult
                    .disabled(String.format(
                            "test method \"%s\" has tag \"%s\" which is on the @ExcludeTags list \"[%s]\", test will be skipped",
                            (element instanceof Method) ? ((Method) element).getName()
                                    : element.getClass().getSimpleName(),
                            tag.get(),
                            tagsToExclude.get().stream().collect(Collectors.joining(","))
                    ));
        }
        return ConditionEvaluationResult.enabled(
                String.format(
                        "test method \"%s\" has tag \"%s\" which is not on the @ExcludeTags list \"[%s]\", test will be run",
                        (element instanceof Method) ? ((Method) element).getName()
                                : element.getClass().getSimpleName(),
                        tag.orElse("<no tag present>"),
                        tagsToExclude.get().stream().collect(Collectors.joining(","))
                ));
    }
}

Create annotation @ExcludeTags

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@ExtendWith(ExcludeTagsCondition.class)
public @interface ExcludeTags {
    String[] value();
}

On your test

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@ExcludeTags({"foo", "bar"})
@SpringBootTest
class AppTest {

    @Test
    @Tag("foo")
    void test1() {
        System.out.println("test1");
    }

    @Test
    @Tag("bar")
    void test2() {
        System.out.println("test2");
    }

    @Test
    @Tag("baz")
    void test3() {
        System.out.println("test3");
    }
}

When you run the test, you should see the following output:

test method "test1" has tag "foo" which is on the @ExcludeTags list "[bar,foo]", test will be skipped

test method "test2" has tag "bar" which is on the @ExcludeTags list "[bar,foo]", test will be skipped

test3

And your test runner should show 1 test passing and 2 skipped.

enter image description here

Now for your test suite:

Create an annotation @MustIncludeTags

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface MustIncludeTags {
    String[] value();
}

Now setup your test suite like so:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
@SelectClasses({MyTestSuite.SetupTests.class, AppTest.class})
@MustIncludeTags({"foo", "bar"})
public class MyTestSuite {

    public static class SetupTests {
    
        @BeforeAll
        public static void beforeClass() {
            ExcludeTagsCondition.setMustIncludeTags(
                    Optional.ofNullable(MyTestSuite.class.getAnnotation(MustIncludeTags.class))
                            .map(MustIncludeTags::value)
                            .map(Arrays::asList)
                            .orElse(new ArrayList<>())
                            .stream()
                            .collect(Collectors.toSet())
            );
        }
    
        @Disabled
        @Test
        void testDummy() {
            // this test needs to be present for the beforeAll to run
        }
    
    }
}

When you run your test suite with the @MustIncludeTags the @ExcludedTags are overridden.

As you can see from the following test execution:

enter image description here

Matthew Madson
  • 1,643
  • 13
  • 24
  • I updated my question, is your answer still valid? Also, when using Junit 5, creating a test suite, is '@RunWith' correct, or shall '@ExtendWith' be used? – neblaz Jun 16 '20 at 08:15
  • @neblaz Updated answer, just tested with Junit 5.5.1 – Matthew Madson Jun 16 '20 at 08:30
  • Will try it, but have you also created a test suite, which runs the annotated methods? The class shall exlude some tagged methods, but the test suite shall run them. See my example in the question. – neblaz Jun 16 '20 at 09:13
  • I just tried it, the specified tags are not executed in MyClassTest class, which is fine. But they are also not executed in the test suite MyClassTestSuiteTest, which is not desired. – neblaz Jun 16 '20 at 09:21
  • Got it, I would probably add a @BeforeClass to your TestSuite and set some global mustRunTag list. Then update my ExcludeTagsCondition to ensure that whatever tags are on the mustRunTag list do not get skipped – Matthew Madson Jun 16 '20 at 09:27
  • 1
    The first thing is to understand, does a mix of JUnit 4 and 5 artifacts has to be used, or can it all be done with JUnit 5 artifacts? '@RunWith' (also '@BeforeClass') as I understand is an old annotation before JUnit 5, does it have to be used with test suites or can '@ExtendWith' be used? – neblaz Jun 16 '20 at 09:50
  • Okay, updated the code with your TestSuite. It's a bit hacky but it gets the job done. – Matthew Madson Jun 16 '20 at 10:10
  • And to answer your question, Test Suites are only available if you use the junit 4 platform engine. Native junit 5 support is still a WIP: https://github.com/junit-team/junit5/issues/744#issuecomment-581324824 Personally I don't use test suites, I have maven decide which tests to run using the surefire plugin. – Matthew Madson Jun 16 '20 at 10:19
  • Well, it seams to work: In MyTestClass the annotated test methods are skipped, correct. But in the test suite all test methods from MyTestClass are run, but should only be the annotated one (with the MustIncludeTags). That must be some logical mistake in ExcludeTagsCondition. – neblaz Jun 16 '20 at 11:31
  • I'm hacking it further, will provide the solution when done. – neblaz Jun 16 '20 at 13:33
  • Cool, if the answer was helpful an upvote would be appreciated =) – Matthew Madson Jun 16 '20 at 17:20
  • First I would like to try a solution without annotations: Creating a super-class, to be able to isolate the tagged methods in one of the sub- classes. The sub-class is then put inside the test suite as a public static class. BUT: Regardless which solution I try, with your concept of annotations (still not finished) or my sub-classing, Maven is not running the test suite. See here: https://stackoverflow.com/questions/62514473/spring-boot-2-3-1-junit-5-maven-3-6-3-maven-lifecycle-test-does-not-run-te perhaps you have a clue? – neblaz Jun 26 '20 at 06:28