跨 junit 测试类重用 spring 应用程序上下文

我们有一些 JUnit 测试用例(集成测试) ,它们在逻辑上被分组到不同的测试类中。

我们能够在每个测试类中加载 Spring 应用程序上下文一次,并为 http://static.springsource.org/spring/docs/current/spring-framework-reference/html/testing.html中提到的 JUnit 测试类中的所有测试用例重用它

然而,我们只是想知道是否有一种方法可以为一组 JUnit 测试类只加载一次 Spring 应用程序上下文。

FWIW,我们使用 Spring3.0.5,JUnit4.5和 Maven 来构建项目。

81435 次浏览

Yes, this is perfectly possible. All you have to do is to use the same locations attribute in your test classes:

@ContextConfiguration(locations = "classpath:test-context.xml")

Spring caches application contexts by locations attribute so if the same locations appears for the second time, Spring uses the same context rather than creating a new one.

I wrote an article about this feature: Speeding up Spring integration tests. Also it is described in details in Spring documentation: 9.3.2.1 Context management and caching.

This has an interesting implication. Because Spring does not know when JUnit is done, it caches all context forever and closes them using JVM shutdown hook. This behavior (especially when you have a lot of test classes with different locations) might lead to excessive memory usage, memory leaks, etc. Another advantage of caching context.

To add to Tomasz Nurkiewicz's answer, as of Spring 3.2.2 @ContextHierarchy annotation can be used to have separate, associated multiple context structure. This is helpful when multiple test classes want to share (for example) in-memory database setups (datasource, EntityManagerFactory, tx manager etc).

For example:

@ContextHierarchy({
@ContextConfiguration("/test-db-setup-context.xml"),
@ContextConfiguration("FirstTest-context.xml")
})
@RunWith(SpringJUnit4ClassRunner.class)
public class FirstTest {
...
}


@ContextHierarchy({
@ContextConfiguration("/test-db-setup-context.xml"),
@ContextConfiguration("SecondTest-context.xml")
})
@RunWith(SpringJUnit4ClassRunner.class)
public class SecondTest {
...
}

By having this setup the context that uses "test-db-setup-context.xml" will only be created once, but beans inside it can be injected to individual unit test's context

More on the manual: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/testing.html#testcontext-ctx-management (search for "context hierarchy")

Basically spring is smart enough to configure this for you if you have the same application context configuration across the different test classes. For instance let's say you have two classes A and B as follows:

@ActiveProfiles("h2")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class A {


@MockBean
private C c;
//Autowired fields, test cases etc...
}


@ActiveProfiles("h2")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class B {


@MockBean
private D d;
//Autowired fields, test cases etc...
}

In this example class A mocks bean C, whereas class B mocks bean D. So, spring considers these as two different configurations and thus would load the application context once for class A and once for class B.

If instead, we'd want to have spring share the application context between these two classes, they would have to look something as follows:

@ActiveProfiles("h2")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class A {


@MockBean
private C c;


@MockBean
private D d;
//Autowired fields, test cases etc...
}


@ActiveProfiles("h2")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class B {


@MockBean
private C c;


@MockBean
private D d;
//Autowired fields, test cases etc...
}

If you wire up your classes like this, spring would load the application context only once either for class A or B depending on which class among the two is ran first in the test suite. This could be replicated across multiple test classes, only criteria is that you should not customize the test classes differently. Any customization that results in the test class to be different from the other(in the eyes of spring) would end up creating another application context by spring.

create your configuaration class like below

@ActiveProfiles("local")
@RunWith(SpringJUnit4ClassRunner.class )
@SpringBootTest(classes ={add your spring beans configuration classess})
@TestPropertySource(properties = {"spring.config.location=classpath:application"})
@ContextConfiguration(initializers = ConfigFileApplicationContextInitializer.class)
public class RunConfigration {


private ClassLoader classloader = Thread.currentThread().getContextClassLoader();


private static final Logger LOG = LoggerFactory.getLogger(S2BXISINServiceTest.class);




//auto wire all the beans you wanted to use in your test classes
@Autowired
public XYZ xyz;
@Autowired
public ABC abc;




}






Create your test suite like below






@RunWith(Suite.class)
@Suite.SuiteClasses({Test1.class,test2.class})
public class TestSuite extends RunConfigration {


private ClassLoader classloader = Thread.currentThread().getContextClassLoader();


private static final Logger LOG = LoggerFactory.getLogger(TestSuite.class);




}

Create your test classes like below

public class Test1 extends RunConfigration {




@Test
public void test1()
{
you can use autowired beans of RunConfigration classes here
}


}




public class Test2a extends RunConfigration {


@Test
public void test2()
{
you can use autowired beans of RunConfigration classes here
}




}

One remarkable point is that if we use @SpringBootTests but again use @MockBean in different test classes, Spring has no way to reuse its application context for all tests.

Solution is to move all @MockBean into an common abstract class and that fix the issue.

@SpringBootTests(webEnvironment = WebEnvironment.RANDOM_PORT, classes = Application.class)
public abstract class AbstractIT {


@MockBean
private ProductService productService;


@MockBean
private InvoiceService invoiceService;


}

Then the test classes can be seen as below

public class ProductControllerIT extends AbstractIT {
// please don't use @MockBean here
@Test
public void searchProduct_ShouldSuccess() {
}


}


public class InvoiceControllerIT extends AbstractIT {
// please don't use @MockBean here
@Test
public void searchInvoice_ShouldSuccess() {
}


}