Running a single test from a Suite with @ClassRule

2019-08-23 23:35发布

To create the environment just once and to avoid inheritance I have defined a JUnit Suite class with a @ClassRule:

@RunWith(Suite.class)               
@Suite.SuiteClasses({               
  SuiteTest1.class              
})      

public class JUnitTest {

    @ClassRule
    private static DockerComposeContainer env = ...


    @BeforeClass
    public static void init(){
        ...
    }

    ...

}

And there's a Test class that uses env in a test method:

public class SuiteTest1 {               

    @Test
    public void method(){
        client.query(...);// Executes a query against docker container


    }
}

When I execute the tests by running the Test Suite everything works as expected. But when I directly try to run (even with IDE) the SuiteTest1 test class, it fails and nothing from the Suite is called (i.e. @ClassRule and @BeforeClass).

Any suggestions on how to achieve also the SuiteTest1 single execution in an good way (without calling static methods of JUnitTest from within the SuiteTest1) ?

1条回答
爱情/是我丢掉的垃圾
2楼-- · 2019-08-24 00:23

Rephrasing the question: you want a JUnit suite with before-all and after-all hooks, which would also run when running the tests one by one (e.g. from an IDE).

AFAIK JUnit 4 provides nothing out-of-the-box for this, but if you're OK with incorporating some Spring third-parties deps (spring-test and spring-context) into your project I can propose a workaround I've been using.

The full example of what is described in this question can be found here.

Solution (using Spring)

We'll use Spring context for implementing our initialization and cleanup. Let's add a base class for our tests:

@ContextConfiguration(initializers = AbstractTestClass.ContextInitializer.class)
public class AbstractTestClass {

    @ClassRule
    public final static SpringClassRule springClassRule = new SpringClassRule();

    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();

    public static class ContextInitializer
            implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext context) {
            System.out.println("Initializing context");

            context.addApplicationListener(
                    (ApplicationListener<ContextClosedEvent>)
                            contextClosedEvent ->
                                    System.out.println("Closing context"));
        }
    }
}

Note the SpringClassRule and SpringMethodRule JUnit rules which enhance our base class with Spring-superpowers (Spring test annotation processing - ContextConfiguration in this case, but there are many more goodies in there - see Spring testing reference for details). You could use SpringRunner for this purpose, but it's a far less flexible solution (thus omitted).

Test classes:

public class TestClass1 extends AbstractTestClass {

    @Test
    public void test() {
        System.out.println("TestClass1 test");
    }
}

public class TestClass2 extends AbstractTestClass {

    @Test
    public void test() {
        System.out.println("TestClass2 test");
    }
}

And the test suite:

@RunWith(Suite.class)
@SuiteClasses({TestClass1.class, TestClass2.class})
public class TestSuite {
}

Output when running the suite (removed Spring-specific logs for brievity):

Initializing context
TestClass1 test
TestClass2 test
Closing context

Output when running a single test (TestClass1):

Initializing context
TestClass1 test
Closing context

A word of explanation

The way this works is because of Spring's context caching. Quote from the docs:

Once the TestContext framework loads an ApplicationContext (or WebApplicationContext) for a test, that context is cached and reused for all subsequent tests that declare the same unique context configuration within the same test suite. To understand how caching works, it is important to understand what is meant by “unique” and “test suite.”

-- https://docs.spring.io/spring/docs/5.1.2.RELEASE/spring-framework-reference/testing.html#testcontext-ctx-management-caching

Beware that you will get another context (and another initialization) if you override the context configuration (e.g. add another context initializer with ContextConfiguration) for any of the classes in the hierarchy (TestClass1 or TestClass2 in our example).

Using beans to share instances

You can define beans in your context. They'll be shared across all tests using the same context. This can be useful for sharing an object across the test suite (a Testcontainers container in your case judging by the tags).

Let's add a bean:

@ContextConfiguration(initializers = AbstractTestClass.ContextInitializer.class)
public class AbstractTestClass {

    @ClassRule
    public final static SpringClassRule springClassRule = new SpringClassRule();

    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();

    public static class ContextInitializer
            implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext context) {
            ADockerContainer aDockerContainer = new ADockerContainer();
            aDockerContainer.start();

            context.getBeanFactory().registerResolvableDependency(
                    ADockerContainer.class, aDockerContainer);

            context.addApplicationListener(
                    (ApplicationListener<ContextClosedEvent>)
                            contextClosedEvent ->
                                    aDockerContainer.stop());
        }
    }
}

And inject it into the test classes:

public class TestClass1 extends AbstractTestClass {

    @Autowired
    private ADockerContainer aDockerContainer;

    @Test
    public void test() {
        System.out.println("TestClass1 test " + aDockerContainer.getData());
    }
}

public class TestClass2 extends AbstractTestClass {

    @Autowired
    private ADockerContainer aDockerContainer;

    @Test
    public void test() {
        System.out.println("TestClass2 test " + aDockerContainer.getData());
    }
}

ADockerContainer class:

public class ADockerContainer {
    private UUID data;

    public void start() {
        System.out.println("Start container");
        data = UUID.randomUUID();
    }

    public void stop() {
        System.out.println("Stop container");
    }

    public String getData() {
        return data.toString();
    }
}

(Example) output:

Start container
TestClass1 test 56ead80b-ec34-4dd6-9c0d-d6f07a4eb0d8
TestClass2 test 56ead80b-ec34-4dd6-9c0d-d6f07a4eb0d8
Stop container
查看更多
登录 后发表回答