由于“集成测试”这个术语被许多不同角色的人使用,可能对不同人代表了不同的意思,这里说的集成测试是指挑选出几个程序单元(通常包括外部系统)将它们装配起来并对它们进行测试。就我个人而言,经常使用集成测试的方式来测试持久化逻辑(比如调用Dao,验证其实现是否按照预想地操作了数据库)或是一些对象是否正确地被spring framework装配起来(比如一些添加在对象上的AOP advices是否起作用了)。
一般来说,既然是集成测试,那么所有相关的程序都会在测试中执行,但某些特定情况下,需要在测试中使用测试替身(这可能是由于依赖的外部系统没有测试环境等原因)或是运行过程中,某个功能的执行需要花费很大力气去准备数据和环境,但其只是一个程序执行的必经点并不是要测试的核心点。这里,介绍几种使用spring framework启动applicationContext后,替换其中被管理的bean为测试替身的方法。
一、手工替换
这里使用一些Toy code来做演示。整个测试使用spring-test框架搭建,它会在测试前自动启动一个按照classpath:config.xml配置的ApplicationContext,接下来可以通过(3)这样的代码从ApplicationContext中获得你需要测试的Bean进行测试。如果你熟悉JMock2的话,从(5)中你应该可以看出SomeApp.returnHelloWorld()依赖SomeInterface.isAvailable(),我们假设这其实现在测试中无法调用(我遇到过一个真实的例子就是一个在线付款的应用,每次调用其中一个供应商提供的接口,意味着需要使用银行卡支付一笔钱,而该供应商并不提供测试环境,我们也不希望每次运行该测试兜里就少掉1块钱)。于是我们在(4)手工的替换掉了该实现。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:config.xml")
public class SomeAppIntegrationTestsUsingManualReplacing {
private Mockery context = new JUnit4Mockery(); (1)
private SomeInterface mock = context.mock(SomeInterface.class); (2)
@Resource(name = "someApp")
private SomeApp someApp; (3)
@Before
public void replaceDependenceWithMock() {
someApp.setDependence(mock); (4)
}
@Test
public void returnsHelloWorldIfDependenceIsAvailable() throws Exception {
context.checking(new Expectations() {
{
allowing(mock).isAvailable();
will(returnValue(true)); (5)
}
});
String actual = someApp.returnHelloWorld();
assertEquals("helloWorld", actual);
context.assertIsSatisfied(); (6)
}
}
这里要注意(6)这步非常重要,因为我们指定了@RunWith(SpringJUnit4ClassRunner.class),而不是@RunWith(JMock.class),所以一些原本由JMock自动处理的工作需要我们手工来完成,其中就包括调用assertIsSatisfied()来验证是否所有的预期都被调用了且没有未预期调用出现。
二、预先准备替换过的ApplicationContext
方案一已经基本达到了我们的目标,但还有些瑕疵:
1.如果需要替换多个对象,有时你可能不得不从ApplicationContext中取出多个对象,再依次通过setter()注入测试替身。这样比较麻烦,而且必须通过硬编码来完成注入(本来应该是由spring来完成的事)。
2.严格来说,这不能叫做替换,因为并不能保证,原来的对象已经被正确地注入到目标中去,比如你忘记编写<property name="beanName" ref="bean" />来完成注入(不知为什么,这件事情经常发生在我身上)。
那怎么能在已经按照生产环境配置装配ApplicationContext的情况下,替换测试替身呢?Spring为我们提供了一个解决方案BeanPostProcessor,根据参考手册中的说法,ApplicationContext会自动检测xml文件中定义的BeanPostProcessor实现,并使用它们来增强其他Bean。
public class PredefinedBeanPostProcessor implements BeanPostProcessor {
public Mockery context = new JUnit4Mockery(); (1)
public SomeInterface mock = context.mock(SomeInterface.class); (2)
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if ("dependence".equals(beanName)) {
return mock;
} else {
return bean;
}
}
}
在这个实现中,我们定义了测试替身(1)(2),并在postProcessAfterInitialization()中根据beanName做了替换。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:config.xml",
"classpath:predefined.xml" }) (1)
public class SomeAppIntegrationTestsUsingPredefinedReplacing {
@Resource(name = "someApp")
private SomeApp someApp;
@Resource(name = "predefined")
private PredefinedBeanPostProcessor fixture;
@Test
public void returnsHelloWorldIfDependenceIsAvailable() throws Exception {
fixture.context.checking(new Expectations() {
{
allowing(fixture.mock).isAvailable();
will(returnValue(true));
}
});
String actual = someApp.returnHelloWorld();
assertEquals("helloWorld", actual);
fixture.context.assertIsSatisfied();
}
}
请注意这个版本的测试,我们在(1)中额外定义了一个xml文件,在其中定义了PredefinedBeanPostProcessor,这样我们可以在生产环境的二进制包中比较简单的去除掉该文件其PredefinedBeanPostProcessor(比如将predefined.xml放在src/test/resources/目录下)。
三、动态替换
方案二比起方案一更“安全”一些,因为没有手工注入的代码,我们只要使用了正确地beanName,基本可以确保config.xml中的Bean的装配是符合预期的。不过这样就是比较麻烦,每当有这样的问题时,都要定义一个Java文件,一个xml文件。如果可以像方案一那样,可以在具体的测试用例中指定要替换哪个测试替身,但又可以像方案二一样,利用spring来实现替换就好了。
public class TestDoubleInjector implements BeanPostProcessor {
private static Map<String, Object> MOCKS = new HashMap<String, Object>(); (1)
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (MOCKS.containsKey(beanName)) {
return MOCKS.get(beanName);
}
return bean;
}
public void addMock(String beanName, Object mock) {
MOCKS.put(beanName, mock);
}
public void clear() {
MOCKS.clear();
}
}
这里我想到了将测试替身保存在一个Map中,在测试开始前,填充该Map,但是测试开始前,ApplicationContext已经启动了,替换的时机已经错过了。这里我在(1)中将这个Map定义为静态变量,然后通过延迟启动ApplicationContext的方式初步完成了目标。
@RunWith(JMock.class)
public class SomeAppIntegrationTestsUsingDynamicReplacing {
private Mockery context = new JUnit4Mockery();
private SomeInterface mock = context.mock(SomeInterface.class);
private SomeApp someApp;
private ConfigurableApplicationContext applicationContext;
private TestDoubleInjector fixture = new TestDoubleInjector(); (1)
@Before
public void replaceDependenceWithMock() {
fixture.addMock("dependence", mock); (2)
applicationContext = new ClassPathXmlApplicationContext(new String[] {
"classpath:config.xml", "classpath:dynamic.xml" }); (3)
someApp = (SomeApp) applicationContext.getBean("someApp");
}
@Test
public void returnsHelloWorldIfDependenceIsAvailable() throws Exception {
context.checking(new Expectations() {
{
allowing(mock).isAvailable();
will(returnValue(true));
}
});
String actual = someApp.returnHelloWorld();
assertEquals("helloWorld", actual);
}
@After
public void clean() {
applicationContext.close();
fixture.clear();
}
}
在这个版本的测试中,利用类似Monostate模式的方法(1)(3),我们在启动ApplicationContext前(3),完成刚才提高的Map的填充,虽然spring实例化的TestDoubleInjector和我们在(1)中实例化的TestDoubleInjector并不是同一个实例,但由于它们共享一个Map,所以在随后启动ApplicationContext的过程中就可以取到我们指定的测试替身了。这样,如果TestDoubleInjector及其xml配置可以被若干个测试共享,不过要注意的是,由于static Map的关系,记得要在测试前/后清空一下,否则可能混入上一个测试中定义的测试替身。
这个方案也有缺点,就是ApplicationContext每次测试都要重建,如果你的测试集中包含了很多这样的测试,而ApplicationContext的规模比较大(其中定义的Bean较多),那么花费的时间就会比较多。当然,方案一和方案二也可能有这个问题,因为它们也都“污染”了ApplicationContext,如果在其他测试中你并不需要替换测试替身,你需要在测试方法中声明@DirtiesContext,这样在测试后,spring会重建ApplicationContext(默认情况下,在一个测试套件中,spring只会创建一次ApplicationContext以节约时间)。
好了,就介绍到这里,虽然我都是以Mock作为例子,但如果你要替换的是Stub,方法也是一样的。如果你有更好的方法,请务必跟帖让我知道,谢谢
参考资料:
http://www.jmock.org 在这里你可以找到Mock的详细信息
http://www.oracle.com/technetwork/articles/entarch/spring-aop-with-ejb5-093994.html 在这里我第一次了解到BeanPostProcessor及其在测试中的用处