作者:江南白衣
注重實效的TDD的確能加快,而不是拖慢開發的進度(片面的追求覆蓋率的全面UnitTest不在此列)
一,可以實現真正分層開發。
二,不需要依賴和頻繁重啟Web Container。
三,手工測試總不免改動資料庫,如何把資料庫恢複到測試前的狀態是件傷腦筋的事情。而Unit Test可以使用自動Rollback機制,巧妙的解決了這件事情。
Spring 下的Unit Test主要關注三個方面:
1. bean的依賴注入
2. 事務控制,Open Session in Test 及預設復原
3. 脫離WebContainer對控制層的測試
1.bean的依賴注入
能不依靠WebContainer來完成ApplicationContext的建立與POJO的依賴注入一向是Spring的得意之處。
String[] paths = { "classpath:applicationContext*.xml" };
ApplicationContext ctx =new ClassPathXmlApplicationContext(paths);
UserDAO dao = (UserDAO) ctx.getBean("userDAO");
如果你連這也覺得麻煩,那麼只要你的testCase繼承於Spring-mock.jar裡的AbstractDependencyInjectionSpringContextTests,實現public String[] getConfigLocations()函數, 並顯式寫一些需要注入的變數的setter函數。
註:因為是AutoWire的,變數名必須等於Spring context檔案裡bean的id。
2.Open Session in Test 及自動Rollback
又是來自Spring這個神奇國度的東西, 你可以讓testCase繼承於AbstractTransactionalDataSourceSpringContextTests,就可以做到Open Session in Test ,解決Hibernate的lazy-load問題;而且接管原來的DAO裡的事務控制定義,通過setDefaultRollback(boolean)方法控制最後復原還是提交,如果預設為復原,則測試產生資料變動不會影響資料庫內資料。
如果不能繼承於這個基類,可以自己簡單編寫,代碼是這樣的:
protected PlatformTransactionManager transactionManager;
protected TransactionStatus transactionStatus;
protected boolean defaultRollback = true;
public void setUp()
{
transactionManager = (PlatformTransactionManager) ctx.getBean("transactionManager");
transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
}
public void tearDown()
{
if (defaultRollback)
transactionManager.rollback(this.transactionStatus);
else
transactionManager.commit(this.transactionStatus);
}
(注,hibernate太奸詐了,如果全部預設復原,只會在session裡幹活,一點不寫資料庫,達不到完全的測試效果。)
BTW.AbstractTransactionalDataSourceSpringContextTests 還通過注入的DataSource建立了一個JDBCTemplate 對象,可以跑SQL幫忙核對Hibernate的結果,但要注意兩者的事務。
3.Controller層的Unit Test
controller層靠Spring提供的MockHttpServletRequest和Response來類比真實的servlet環境,並且spring 2.0了加了一個AbstractModelAndViewTests,提供一些檢測傳回值的utils函數。
protected XmlWebApplicationContext ctx;
protected MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
protected MockHttpServletResponse response = new MockHttpServletResponse();
protected Controller controller = null;
protected ModelAndView mv = null;
public void setUp()
{
String[] paths = {"applicationContext*.xml","myappfuse-servlet.xml"};
ctx = new XmlWebApplicationContext();
ctx.setConfigLocations(paths);
ctx.setServletContext(new MockServletContext("")); ctx.refresh();
controller = (CustomerController) ctx.getBean("customerController");
//再加上前文的事務控制的代碼
}
public void testCustomerList() throws Exception
{
request.setRequestURI("/customer.do");
request.addParameter("action", "listView");
mv = controller.handleRequest(request, response);
assertModelAttributeAvailable(mv, "customers");
}
4.進一步簡化
一來這兩個基類的名字都太長了。
二來有一些公用的context檔案的定義。
所以可以再抽象了幾個基類,分別是DAOTestCase,ControllerTestCase。
5. EasyMock
MockObject是一樣徹底分層開發的好東西,而且使用上沒什麼難度。而且已不再存在只支援介面不支援Class的限制。
//設定BookManager MockObject
bookManagerMockControl = MockClassControl.createControl(BookManager.class);
bookManagerMock = (BookManager) bookManagerMockControl.getMock();
controller.setBookManager(bookManagerMock);
//錄製getAllBook()和getCategorys方法的期望值
bookManagerMock.getAllBook();
bookManagerMockControl.setReturnValue(new ArrayList());
bookManagerMockControl.replay();
//執行操作
mv = controller.handleRequest(request, response);
//驗證結果
assertModelAttributeAvailable(mv, "books");
Easy Mock VS JMock:
JMock 要求TestCase繼承於MockObjectTestCase太霸道了。妨礙了我繼承於Spring2.0的ModelAndViewTestCase和使用MockDao,RealDao並行的繼承體系。因此採用沒那麼霸道的easyMock。
另外,easyMock的指令碼錄製雖不如jmock那麼優美,但勝在簡短易讀。jmock那句太長了 。
6. 顯示層測試
還有,顯示層至今沒有什麼好的UnitTest方法,無論是不成才的httpUnit們還是笨重的GUI test工具。Appfuse一直用的那個ThoughtWork那個Selenium和J3Unit的效果不知如何, 其中J3Unit號稱支援prototype。