日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

Spring項目單元測試

 昵稱27831725 2018-01-12

Unit test與developing之間的矛盾由來已久,unit test帶來的時間成本是否能超過其對質量的提升,每個團隊的結果都不相同。比如團結成熟度很高,那么一些簡單的unit test或許帶來不了什么收益;但是如果團隊比較年輕,成員也有很多經驗不夠豐富的開發(fā)人員,不可避免會有一些低級bug出現,unit test的收益就會相對明顯。做不做都是這個團隊的取舍。

本文針對Spring項目的unit test提出幾種方案,并加以分析。Spring project的核心是bean,所以unit test不可避免需要能夠生產“bean”,因此有兩種實現方式:

  1. 加載spring配置,類似項目容器加載
  2. mock spring bean,對bean的調用方法進行攔截

依賴spring bean的unit test測試方案

這種方案的還原度最高,與真實運行的差別僅僅是容器,服務器環(huán)境等因素。常見的實現方案通過Spring Unit實現,常見實現代碼如下。
  1. @RunWith(SpringJUnit4ClassRunner.class)  
  2. @ContextConfiguration(locations = {"classpath:spring-test-config.xml","xxx.xml"})  
  3. @TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)  
  4. @TestExecutionListeners( { xxxListener.class,xxxListener.class })  
  5. public class BaseTest extends AbstractTransactionalJUnit4SpringContextTests  
  6. public class BaseTest{  
  7.     //... 公用代碼部分  
  8. }  
spring配置文件通過@ContextConfiguration注入,事務通過@TransactionConfiguration聲明。如果還有一些listener,可以通過@TestExecutionListeners方式注入。基本上可以滿足測試需求。
簡單的action示例,service同理。
  1. public class xxxTest extends BaseTest{  
  2.       
  3.     @Autowired  
  4.     private xxxBean xxxbean;  
  5.   
  6.     @Test  
  7.     public void tesr()  {  
  8.         MockHttpServletRequest request = new MockHttpServletRequest();  
  9.         request.setMethod("POST");  
  10.         request.addParameter(xxx,xxx);  
  11.         request.setServletPath(xxx);  
  12.         xxxbean.test(request);  
  13.         //....  
  14.         //multipart request  
  15.         //MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest();  
  16.         //request.addFile(new MockMultipartFile("xxx.png","xxx.png",null, new FileInputStream("xxx.png")));  
  17.     }  
  18. }  


如果涉及作用域問題,spring mock也提供支持。
  1. public class xxxTest extends BaseTest{  
  2.     @Autowired  
  3.     private  xxxController xxxController;  
  4.       
  5.     public ClassA test() {  
  6.         RequestContextListener listener = new RequestContextListener();  
  7.         MockServletContext context = new MockServletContext();  
  8.         MockHttpServletRequest request = new MockHttpServletRequest();  
  9.         MockHttpServletResponse response = new MockHttpServletResponse();  
  10.         request.setMethod("POST");  
  11.         request.addParameter("xxx", "xxx");  
  12.         request.setServletPath("xxx");  
  13.         listener.requestInitialized(new ServletRequestEvent(context, request));  
  14.         ClassA classa = xxxController.getClassA(request, response);  
  15.         Assert.assertNotNull(classa);  
  16.         return classa  
  17.     }  
  18. }  


上面的示例中,需要注意的是,所有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),想要打印兩串字符,一串數字和一串字母。
  1. public class Main {  
  2.     public static void main(String[] args) {  
  3.         Printer printer = new HpPrinter();  
  4.         String result1 = printer.print("abc");  
  5.         String result2 = pringter.print("1234");  
  6.         System.out.println("result1:" + result1);  
  7.         System.out.println("result2:" + result2);  
  8.     }  
  9. }  
  10.   
  11. public interface Printer {  
  12.       
  13.     public String print(String message);  
  14. }  
  15.   
  16. public class HpPrinter implements Printer{  
  17.   
  18.     @Override  
  19.     public String print(String message) {  
  20.         return message;  
  21.     }  
  22.   
  23. }  
輸出


然而某一天老板突然下了個指令,不讓打印字母了(不要問為什么...)。實現方案很多,這里用proxy實現。
  1. public class PrintProxy {  
  2.     private Printer printer;  
  3.     public PrintProxy(Printer printer){  
  4.         this.printer = printer;  
  5.     }  
  6.       
  7.     public Printer create(){  
  8.         final Class<?>[] interfaces = new Class[]{Printer.class};  
  9.         final PrinterInvacationHandler handler = new PrinterInvacationHandler(printer);  
  10.           
  11.         return (Printer)Proxy.newProxyInstance(Printer.class.getClassLoader(), interfaces, handler);  
  12.     }  
  13. }  
  14.   
  15. public class PrinterInvacationHandler implements InvocationHandler{  
  16.   
  17.     private final Printer printer;  
  18.     public PrinterInvacationHandler(Printer printer){  
  19.         this.printer = printer;  
  20.     }  
  21.     @Override  
  22.     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
  23.         System.out.println("**** before running...");  
  24.         if (method.getName().equals("print") && args.length == 1 && args[0].toString().equals("abc")) {  
  25.             return "打印機無法打印字母:abc";  
  26.         }  
  27.         Object ret = method.invoke(printer, args);  
  28.         System.out.println("**** after running...");  
  29.         return ret;  
  30.     }  
  31. }  
增加了這個代理以后,Main不要直接打印,而是交由這個代理去管理。
  1. public class Main {  
  2.     public static void main(String[] args) {  
  3.         Printer printer = new HpPrinter();  
  4.         PrintProxy proxy = new PrintProxy(printer);  
  5.           
  6.         Printer proxyOjb = proxy.create();  
  7.         String result1 = proxyOjb.print("abc");  
  8.         String result2 = proxyOjb.print("1234");  
  9.         System.out.println("result1:" + result1);  
  10.         System.out.println("result2:" + result2);  
  11.     }  
  12. }  
輸出


可以看到abc被攔截了。Spring AOP,mockito的設計也是如此。言歸正傳,如果使用mockito。
Spring service常見的結構是 servie -> dao。當我們測試一個service方法時,mock這個dao的返回。
  1. @Mock  
  2. private xxxDao dao;  
  3. @InjectMocks  
  4. private xxxServiceImpl xxxService;//注意這是實例,不是接口  
  5.   
  6. @Test  
  7. public void test(){  
  8.     MockitoAnnotations.initMocks(this);  
  9.     ModelA a = new ModelA();  
  10.     Mockito.when(dao.methodB(Mockito.anyString())).thenReturn(a);  
  11.     ModelA b = xxxService.methodA("test");  
  12.     //...  
  13. }  


如果涉及到static,可以引入PowerMockito。下面是個apache validate的例子。
  1. @RunWith(PowerMockRunner.class)  
  2. @PrepareForTest({Validate.class})  
  3. public class xxxMockTest {  
  4.     @Before  
  5.     public void setup(){  
  6.         MockitoAnnotations.initMocks(this);  
  7.         PowerMockito.mockStatic(Validate.class);  
  8.         try {  
  9.             PowerMockito.doNothing().when(Validate.class, "validState",false, "xxx");  
  10.         } catch (Exception e) {  
  11.             // TODO Auto-generated catch block  
  12.             e.printStackTrace();  
  13.         }  
  14.     }  
  15. }  
再復雜一些,如果通過static方法調用時,依賴一個spring bean。
  1. @RunWith(PowerMockRunner.class)  
  2. @PrepareForTest({SpringContextHolder.class})  
  3. public class BaseMockTest {  
  4.     @Before  
  5.     public void setup(){  
  6.         MockitoAnnotations.initMocks(this);  
  7.   
  8.         PowerMockito.mockStatic(SpringContextHolder.class);  
  9.         BDDMockito.given(SpringContextHolder.getBean(xxx.class)).willReturn(new xxxImpl());  
  10.     }  
  11. }  




    本站是提供個人知識管理的網絡存儲空間,所有內容均由用戶發(fā)布,不代表本站觀點。請注意甄別內容中的聯系方式、誘導購買等信息,謹防詐騙。如發(fā)現有害或侵權內容,請點擊一鍵舉報。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多