测试通知代码
您可能注意到了,代码不依赖于TradeDao或YahooFeed。这样就可以使用模仿对象完全独立地测试这个组件。模仿对象测试方法允许在组件执行之前声明期望,然后验证这些期望在组件调用期间是否得到满足。要了解有关模仿测试的更多信息,请参见 “参考资料”部分。下面我们将会使用jMock框架,该框架提供了一个灵活且功能强大的API来声明期望。
测试和实际的应用程序使用相同的Spring bean配置是个不错的主意,但是对于特定组件的测试来说,不能使用实际的依赖性,因为这会破坏组件的孤立性。然而,Spring允许在创建Spring 的应用程序上下文时指定一个BeanPostProcessor,从而置换选中的bean和依赖性。在这个例子中,可以使用模仿对象的一个Map,这些模仿对象是在测试代码中创建的,用于置换Spring配置中的bean:
public class StubPostProcessor implements BeanPostProcessor {
private final Map stubs;
public StubPostProcessor( Map stubs) {
this.stubs = stubs;
}
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if(stubs.containsKey(beanName)) return stubs.get(beanName);
return bean;
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
}
在测试用例类的setUp()方法中,我们将使用baseTradeManager和yahooFeed组件的模仿对象来初始化 StubPostProcessor,而这两个组件是使用jMock API创建的。然后,我们就可以创建ClassPathXmlApplicationContext(配置其使用BeanPostProcessor)来实例化一个tradeManager组件。产生的tradeManager组件将使用模仿后的依赖性。
这种方法不仅允许孤立要测试的组件,还可以确保在Spring bean配置中正确定义通知。实际上,要在不模拟大量容器基础架构的情况下使用这样的方法来测试在EJB组件中实现的业务逻辑是不可能的:
public class ForeignTradeAdviceTest extends TestCase {
TradeManager tradeManager;
private Mock baseTradeManagerMock;
private Mock yahooFeedMock;
protected void setUp() throws Exception {
super.setUp();
baseTradeManagerMock = new Mock(TradeManager.class, "baseTradeManager");
TradeManager baseTradeManager = (TradeManager) baseTradeManagerMock.proxy();
yahooFeedMock = new Mock(TradeManager.class, "yahooFeed");
TradeManager yahooFeed = (TradeManager) yahooFeedMock.proxy();
Map stubs = new HashMap();
stubs.put("yahooFeed", yahooFeed);
stubs.put("baseTradeManager", baseTradeManager);
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext(CTX_NAME);
ctx.getBeanFactory().addBeanPostProcessor(new StubPostProcessor(stubs));
tradeManager = (TradeManager) proxyFactory.getProxy();
}
...
在实际的testAdvice()方法中,可以为模仿对象指定期望并验证(例如)baseTradeManager上的getPrice()方法是否返回null,然后yahooFeed上的getPrice()方法也将被调用:
public void testAdvice() throws Throwable {
String symbol = "testSymbol";
BigDecimal expectedPrice = new BigDecimal("0.222");
baseTradeManagerMock.expects(new InvokeOnceMatcher()).method("getPrice")
.with(new IsEqual(symbol)).will(new ReturnStub(null));
yahooFeedMock.expects(new InvokeOnceMatcher()).method("getPrice")
.with(new IsEqual(symbol)).will(new ReturnStub(expectedPrice));
BigDecimal price = tradeManager.getPrice(symbol);
assertEquals("Invalid price", expectedPrice, price);
baseTradeManagerMock.verify();
yahooFeedMock.verify();
}
这段代码使用jMock约束来指定, baseTradeManagerMock期望只使用一个等于symbol的参数调用getPrice()方法一次,而且这次调用将返回null。类似地,yahooFeedMock也期望对同一方法只调用一次,但是返回expectedPrice。这允许在setUp()方法中运行所创建的 tradeManager组件,并断言返回的结果。
这个测试用例很容易参数化,从而涵盖所有可能的用例。注意,当组件抛出异常时,可以很容易地声明期望。
| 测试 | baseTradeManager | yahooFeed | 期望 | |||||
|---|---|---|---|---|---|---|---|---|
| 调用 | 返回 | 抛出 | 调用 | 返回 | 抛出 | 结果t | 异常 | |
| 1 | true | 0.22 | - | false | - | - | 0.22 | - |
| 2 | true | - | e1 | false | - | - | - | e1 |
| 3 | true | null | - | true | 0.33 | - | 0.33 | - |
| 4 | true | null | - | true | null | - | null | - |
| 5 | true | null | - | true | - | e2 | - | e2 |
可以使用这个表更新测试类,使其使用一个涵盖了所有可能场景的参数化序列:
...
public static TestSuite suite() {
BigDecimal v1 = new BigDecimal("0.22");
BigDecimal v2 = new BigDecimal("0.33");
RuntimeException e1 = new RuntimeException("e1");
RuntimeException e2 = new RuntimeException("e2");
TestSuite suite = new TestSuite(ForeignTradeAdviceTest.class.getName());
suite.addTest(new ForeignTradeAdviceTest(true, v1, null, false, null, null, v1, null));
suite.addTest(new ForeignTradeAdviceTest(true, null, e1, false, null, null, null, e1));
suite.addTest(new ForeignTradeAdviceTest(true, null, null, true, v2, null, v2, null));
suite.addTest(new ForeignTradeAdviceTest(true, null, null, true, null, null, null, null));
suite.addTest(new ForeignTradeAdviceTest(true, null, null, true, null, e2, null, e2));
return suite;
}
public ForeignTradeAdviceTest(
boolean baseCall, BigDecimal baseValue, Throwable baseException,
boolean yahooCall, BigDecimal yahooValue, Throwable yahooException,
BigDecimal expectedValue, Throwable expectedException) {
super("test");
this.baseCall = baseCall;
this.baseWill = baseException==null ?
(Stub) new ReturnStub(baseValue) : new ThrowStub(baseException);
this.yahooCall = yahooCall;
this.yahooWill = yahooException==null ?
(Stub) new ReturnStub(yahooValue) : new ThrowStub(yahooException);
this.expectedValue = expectedValue;
this.expectedException = expectedException;
}
public void test() throws Throwable {
String symbol = "testSymbol";
if(baseCall) {
baseTradeManagerMock.expects(new InvokeOnceMatcher())
.method("getPrice").with(new IsEqual(symbol)).will(baseWill);
}
if(yahooCall) {
yahooFeedMock.expects(new InvokeOnceMatcher())
.method("getPrice").with(new IsEqual(symbol)).will(yahooWill);
}
try {
BigDecimal price = tradeManager.getPrice(symbol);
assertEquals("Invalid price", expectedValue, price);
} catch(Exception e) {
if(expectedException==null) {
throw e;
}
}
baseTradeManagerMock.verify();
yahooFeedMock.verify();
}
public String getName() {
return super.getName()+" "+
baseCalled+" "+baseValue+" "+baseException+" "+
yahooCalled+" "+yahooValue+" "+yahooException+" "+
expectedValue+" "+expectedException;
}
...
在更复杂的情况下,上面的测试方法可以很容易地扩展为大得多的输入参数集合,而且它仍然会立刻运行且易于管理。此外,把所有参数移入一个外部配置文件或者甚至Excel电子表格是合理的做法,这些配置文件或电子表格可以由QA团队管理,或者直接根据需求生成。
