Unit test與developing之間的矛盾由來已久,unit test帶來的時間成本是否能超過其對質量的提升,每個團隊的結果都不相同。比如團結成熟度很高,那么一些簡單的unit test或許帶來不了什么收益;但是如果團隊比較年輕,成員也有很多經驗不夠豐富的開發(fā)人員,不可避免會有一些低級bug出現,unit test的收益就會相對明顯。做不做都是這個團隊的取舍。
本文針對Spring項目的unit test提出幾種方案,并加以分析。Spring project的核心是bean,所以unit test不可避免需要能夠生產“bean”,因此有兩種實現方式:
- 加載spring配置,類似項目容器加載
- mock spring bean,對bean的調用方法進行攔截
依賴spring bean的unit test測試方案
這種方案的還原度最高,與真實運行的差別僅僅是容器,服務器環(huán)境等因素。常見的實現方案通過Spring Unit實現,常見實現代碼如下。
- @RunWith(SpringJUnit4ClassRunner.class)
- @ContextConfiguration(locations = {"classpath:spring-test-config.xml","xxx.xml"})
- @TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
- @TestExecutionListeners( { xxxListener.class,xxxListener.class })
- public class BaseTest extends AbstractTransactionalJUnit4SpringContextTests
- public class BaseTest{
- //... 公用代碼部分
- }
spring配置文件通過@ContextConfiguration注入,事務通過@TransactionConfiguration聲明。如果還有一些listener,可以通過@TestExecutionListeners方式注入。基本上可以滿足測試需求。
簡單的action示例,service同理。
- public class xxxTest extends BaseTest{
-
- @Autowired
- private xxxBean xxxbean;
-
- @Test
- public void tesr() {
- MockHttpServletRequest request = new MockHttpServletRequest();
- request.setMethod("POST");
- request.addParameter(xxx,xxx);
- request.setServletPath(xxx);
- xxxbean.test(request);
- //....
- //multipart request
- //MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest();
- //request.addFile(new MockMultipartFile("xxx.png","xxx.png",null, new FileInputStream("xxx.png")));
- }
- }
如果涉及作用域問題,spring mock也提供支持。
- public class xxxTest extends BaseTest{
- @Autowired
- private xxxController xxxController;
-
- public ClassA test() {
- RequestContextListener listener = new RequestContextListener();
- MockServletContext context = new MockServletContext();
- MockHttpServletRequest request = new MockHttpServletRequest();
- MockHttpServletResponse response = new MockHttpServletResponse();
- request.setMethod("POST");
- request.addParameter("xxx", "xxx");
- request.setServletPath("xxx");
- listener.requestInitialized(new ServletRequestEvent(context, request));
- ClassA classa = xxxController.getClassA(request, response);
- Assert.assertNotNull(classa);
- return classa
- }
- }
上面的示例中,需要注意的是,所有mock的對象的屬性,都要通過手動set。比如request的servletpath,multipart file的 originName。
這種方案還原度很高,但是也帶來了弊端,比如datasource。spring源生的datasource是不支持多數據庫的,需要切換或者代碼端控制。而且從unit test的角度分析,測試邏輯不應該依賴于datasource(根據unit test的專一性,datasource應該有自己的unit test)。
查閱資料發(fā)現有一種方案是采用h2代替真實的datasource,這樣整個測試過程的數據都在內存里面,并不依賴真實db。筆者未實踐這種方案。
第一種方案的核心思想是還原程序的運行環(huán)境,從真實測試過來來看,每個unit test都需要加載spring環(huán)境,帶來的結果是unit test運行時間過長。如果依賴datasource,某些dirty data有可能會影響測試結果。在這方面,mock test的方式執(zhí)行上更快。mock的框架很多,比如jmock,easymock,mockito等。這里筆者采用的是mockito + powermockito。
Mock的思想比較接近unit test,不關心method的依賴。比如我有一個MethodA,其實現依賴于接口B和C,其中C又依賴接口D。在第一種方案中,該unit test需要執(zhí)行完B、C和D才能完成測試,但是其實B、C和D應該都有自己的unit test,而且A并不關心依賴接口的實現。這里會出現大量的重復測試,并且如果B、C和D中任意一個接口存在缺陷,會導致A測試無法通過。
采用Mock 后的結構如下。A不在關心B和C的實現,A只需要根據需求mockB和C的返回結果即可。理論上,只要B和C的返回正確,A的邏輯就算正確。至于B和C自身是否有問題,應該交由B和C的unit test測試。這樣才能體現職責單一。
Mockito的資料網上有很多,原理分析google和百度都有。其核心是stud和proxy。通過某種手段(尚未分析源碼)記錄mock的方法,通過proxy攔截其真實執(zhí)行,返回一個預先設置的值,從而達到mock的效果。
做個簡單的demo。我現在有一個打印機(Interface Printer),想要打印兩串字符,一串數字和一串字母。
- public class Main {
- public static void main(String[] args) {
- Printer printer = new HpPrinter();
- String result1 = printer.print("abc");
- String result2 = pringter.print("1234");
- System.out.println("result1:" + result1);
- System.out.println("result2:" + result2);
- }
- }
-
- public interface Printer {
-
- public String print(String message);
- }
-
- public class HpPrinter implements Printer{
-
- @Override
- public String print(String message) {
- return message;
- }
-
- }
輸出
然而某一天老板突然下了個指令,不讓打印字母了(不要問為什么...)。實現方案很多,這里用proxy實現。
- public class PrintProxy {
- private Printer printer;
- public PrintProxy(Printer printer){
- this.printer = printer;
- }
-
- public Printer create(){
- final Class<?>[] interfaces = new Class[]{Printer.class};
- final PrinterInvacationHandler handler = new PrinterInvacationHandler(printer);
-
- return (Printer)Proxy.newProxyInstance(Printer.class.getClassLoader(), interfaces, handler);
- }
- }
-
- public class PrinterInvacationHandler implements InvocationHandler{
-
- private final Printer printer;
- public PrinterInvacationHandler(Printer printer){
- this.printer = printer;
- }
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- System.out.println("**** before running...");
- if (method.getName().equals("print") && args.length == 1 && args[0].toString().equals("abc")) {
- return "打印機無法打印字母:abc";
- }
- Object ret = method.invoke(printer, args);
- System.out.println("**** after running...");
- return ret;
- }
- }
增加了這個代理以后,Main不要直接打印,而是交由這個代理去管理。
- public class Main {
- public static void main(String[] args) {
- Printer printer = new HpPrinter();
- PrintProxy proxy = new PrintProxy(printer);
-
- Printer proxyOjb = proxy.create();
- String result1 = proxyOjb.print("abc");
- String result2 = proxyOjb.print("1234");
- System.out.println("result1:" + result1);
- System.out.println("result2:" + result2);
- }
- }
輸出
可以看到abc被攔截了。Spring AOP,mockito的設計也是如此。言歸正傳,如果使用mockito。
Spring service常見的結構是 servie -> dao。當我們測試一個service方法時,mock這個dao的返回。
- @Mock
- private xxxDao dao;
- @InjectMocks
- private xxxServiceImpl xxxService;//注意這是實例,不是接口
-
- @Test
- public void test(){
- MockitoAnnotations.initMocks(this);
- ModelA a = new ModelA();
- Mockito.when(dao.methodB(Mockito.anyString())).thenReturn(a);
- ModelA b = xxxService.methodA("test");
- //...
- }
如果涉及到static,可以引入PowerMockito。下面是個apache validate的例子。
- @RunWith(PowerMockRunner.class)
- @PrepareForTest({Validate.class})
- public class xxxMockTest {
- @Before
- public void setup(){
- MockitoAnnotations.initMocks(this);
- PowerMockito.mockStatic(Validate.class);
- try {
- PowerMockito.doNothing().when(Validate.class, "validState",false, "xxx");
- } catch (Exception e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
再復雜一些,如果通過static方法調用時,依賴一個spring bean。
- @RunWith(PowerMockRunner.class)
- @PrepareForTest({SpringContextHolder.class})
- public class BaseMockTest {
- @Before
- public void setup(){
- MockitoAnnotations.initMocks(this);
-
- PowerMockito.mockStatic(SpringContextHolder.class);
- BDDMockito.given(SpringContextHolder.getBean(xxx.class)).willReturn(new xxxImpl());
- }
- }
|