spring2.5最大的特色就是全面使用annotation代替xml配置,包括IOC Container、springMVC和 TestContext測試框架等,給我們開發帶來了極大的便利。springMVC的新特性在這篇文章裡面已經有了比較詳盡的介紹,而對於spring的新TestContext測試框架,大家也可以從這裡得到詳細的例子說明,有興趣的可以去仔細閱讀,本文不再贅述。總而言之,通過spring2.5提供的 annotation,我們可以讓我們的類——包括controller,Test等職責特殊的類——更 POJO 化,更易於測試,也提高了 TestCase的開發效率。
在開發過程中,我們通常需要mock特定的對象來測試預期行為,或者使用stub對象來提高單元測試效率。最常見的例子就是在多層webapp中,在controller類的測試方法裡mock或 stub底層dao類的方法,從而減輕單元測試時數據庫操作的開銷,加快單元測試速率。至於Reflection,已不是java的新概念了,各樣框架基本上都有使用Reflection來增強Runtime的動態性。而java5裡Reflection效率的提升和annotation的引入,更是極大地提高java語言的動態性,讓開發人員得到更多Runtime的靈活性。本文將演示如何使用spring2.5和Reflection簡化測試中的 mock,使用的JUnit框架是JUnit4.4,mock框架是Easymock2.4。
讓我們先看看最原始的使用mock對象的測試(假設基於jdk5進行開發,使用了包括static import,varargs等新特性):
import static org.easymock.EasyMock.*;
public void HelloworldTest extends AbstractSingleSpringContextTests {
private Foo foo = createMock(Foo.class);
private Bar bar = createMock(Bar.class);
private Helloworld helloworld;
@Before
public void before() {
reset(foo, bar);
helloworld = new Helloworld(foo, bar);
}
@After
public void after() {
verify(foo, bar);
}
@Test
public void shouldSayHello() {
//set expectations about foo/bar
replay(foo, bar);
helloworld.sayHello();
//assert verification
}
//
}
可以看到,因為使用了 Spring 老版本的 TestContext,上面的代碼至少有兩個方面是需要加強的:
1. 需要大量的 mock 對象創建操作,與真正的 Test Case 無關的繁瑣代碼,而且還引入了對Spring Context Test 類的繼承依賴
2. 針對不同的 Test 類,因為用到不同的 mock 對象,每次都需要顯式去指明 reset/replay/verify 用到的 mock 對象
針對上面的兩個問題,我們有相應的解決方案來改進:
1. 使用spring來替我們創建mock對象,由spring IOC Container在runtime注入需要的mock對象
2. 提供更通用的rest/replay/verify機制來驗證mock對象,而不是每個 Test 類都需要單獨處理
1. 每個mock對象都需要手工創建麼?答案當然是否定的,我們有FactoryBean。通過在配置文件中指定bean的定義,讓spring來替我們創建mock對象。如下是針對Foo類的定義:
<bean id="mockFoo" class="org.easymock.EasyMock" factory-method="createMock">
<constructor-arg index="0" value="Foo"/>
</bean>
< /constructor-arg>
與此同時,Spring TestContext框架提供了 @ContextConfiguration annotation 允許開發人員手工指定 Spring 配置文件所在的位置。這樣,開發過程中,如果開發人員遵循比較好的配置文件組織結構,可以維護一套只用於測試的對象關系配置,裡面只維護測試用到的 mock 對象,以及測試中用到的對 mock 對象有依賴關系的對象。在產品代碼中則使用另一套配置文件,配置真實的業務對象。
JUnit4.4 之後,Test 類上可以通過 @RunWith 注解指定測試用例的 TestRunner ,Spring TestContext框架提供了擴展於 org.junit.internal.runners.JUnit4ClassRunner 的 SpringJUnit4ClassRunner,它負責總裝 Spring TestContext 測試框架並將其統一到 JUnit 4.4 框架中。這樣,你可以把 Test 類上的關於 Spring Test 類的繼承關系去掉,並且使用 JUnit4 之後引入的 annotation 去掉其他任何 JUnit3.8 需要的約定和方法繼承,讓 Test 類更加 POJO。
Test 類也是“純正” 的 java 對象,自然也可以通過 Spring 來管理依賴關系:在 Test 類的成員變量上加上 @Autowired 聲明,使用 SpringJUnit4ClassRunner 運行 Test Case。Spring 會很聰明地幫助我們擺平 Test 依賴的對象,然後再運行已經“合法”的 Test Case,只要你在用於測試的配置文件裡面定義了完整的依賴關系,一如其他正常對象。
<bean id="Helloword" class="Helloworld" autowire="byType"/>
這樣,經過上面三點變化,例子代碼變成了這樣:
import static org.easymock.EasyMock.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("test-context.xml")
public void HelloworldTest {
@Autowired
private Foo foo;
@Autowired
private Bar bar;
@Autowired
private Helloworld helloworld;
@Before
public void before() {
reset(foo, bar);
}
@After
public void after() {
verify(foo, bar);
}
@Test
public void shouldSayHello() {
//set expectations about foo/bar
replay(foo, bar);
helloworld.sayHello();
//assert verification
}
//
}
< bean id="Helloword" class="Helloworld" autowire="byType">
2. 現在看上去是不是好多了?嗯,對象間的依賴關系和mock對象的創建都由 Spring 來替我們維護,再也不用費心了。不過,reset/verify 是不是還是看上去那麼舒服?我們觀察一下,通常為了簡化對 mock 對象的驗證,我們對 Test 類中使用到的 mock 對象都是一起reset/replay /verify,要是能有resetAll()/replayAll()/verifyAll()方法就好了,也省得不同的 Test 類寫一大串對不同的 Mock 對象驗證的方法。OK,這時候我們就要借助 Reflection 來完成這項任務了:通過 Reflection 得到 Test 類中所有加上 @Autowired 聲明的成員變量,驗證它們是不是由代理或者字節碼增強,從而得到該 Test 類的所有由 Spring 創建的 mock 對象,進行 reset/replay/verify。
根據這個思路,我們引入這樣一個 mock 測試的Helper類:
import static org.easymock.EasyMock.*;
final class MockTestHelper {
public static void resetAll(Object testObject) {
reset(getDeclaredMockedFields(testObject));
}
public static void verifyAll(Object testObject) {
verify(getDeclaredMockedFields(testObject));
}
public static void replayAll(Object testObject) {
replay(getDeclaredMockedFields(testObject));
}
private static Object[] getDeclaredMockedFields(Object testObject) {
Field[] declaredFields = testObject.getClass().getDeclaredFields();
List declaredMockedFields = new ArrayList();
for (Field field : declaredFields) {
if (field.isAnnotationPresent(Autowired.class)) {
boolean isAccessible = field.isAccessible();
try {
field.setAccessible(true);
Object value = field.get(testObject);
if (isClassProxy(value.getClass())) {
declaredMockedFields.add(value);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
finally {
field.setAccessible(isAccessible);
}
}
}
return declaredMockedFields.toArray();
}
private static boolean isClassProxy(Class clazz) {
String className = clazz.getName();
return className.contains("$Proxy") || className.contains("$$EnhancerByCGLIB$$");
}
}
好了,有了這麼一個 Helper 類,寫 mock 對象的Test 類就簡單了許多。還是以上面的例子為例,經過這麼一重構,變成如下:
這樣看起來就好多了,以後不管在 Test 類裡面添加多少個 Test 類需要的 mock 對象,我們都不需要再修改對 mock 對象的驗證了,Helper類會自動< bean id="Helloword" class="Helloworld" autowire="byType">幫我們完成所有的工作。& lt;br />
綜上所述,使用Spring2.5裡面引入的 Test Cntext 和 annotations 的確幫助我們減輕了大量的測試代碼量,而且讓我們的 Test 類更加POJO,更易於讓人理解其職責,成為對 feature 的 specification。而 Reflection的小技巧,則能很方便的改進原來代碼中不夠動態的地方,進一步簡化代碼量和維護難度。當然我們可以看到,即使這樣,代碼裡面還是有不少resetAll/replayAll/verifyAll的地方,作為 mock 框架帶來的一些約束,我們沒有辦法來省略。這裡推薦一種新的 mock 框架—— mockito,是有我的外國同事開發的,它不僅把mock、stub、spy等double的概念區分更清楚,而且讓我們的 mock 測試更易寫,更易讀。