分享

Spring项目单元测试

 昵称27831725 2018-01-12

Unit test与developing之间的矛盾由来已久,unit test带来的时间成本是否能超过其对质量的提升,每个团队的结果都不相同。比如团结成熟度很高,那么一些简单的unit test或许带来不了什么收益;但是如果团队比较年轻,成员也有很多经验不够丰富的开发人员,不可避免会有一些低级bug出现,unit test的收益就会相对明显。做不做都是这个团队的取舍。

本文针对Spring项目的unit test提出几种方案,并加以分析。Spring project的核心是bean,所以unit test不可避免需要能够生产“bean”,因此有两种实现方式:

  1. 加载spring配置,类似项目容器加载
  2. mock spring bean,对bean的调用方法进行拦截

依赖spring bean的unit test测试方案

这种方案的还原度最高,与真实运行的差别仅仅是容器,服务器环境等因素。常见的实现方案通过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)。
查阅资料发现有一种方案是采用h2代替真实的datasource,这样整个测试过程的数据都在内存里面,并不依赖真实db。笔者未实践这种方案。

第一种方案的核心思想是还原程序的运行环境,从真实测试过来来看,每个unit test都需要加载spring环境,带来的结果是unit test运行时间过长。如果依赖datasource,某些dirty data有可能会影响测试结果。在这方面,mock test的方式执行上更快。mock的框架很多,比如jmock,easymock,mockito等。这里笔者采用的是mockito + powermockito。

Mock的思想比较接近unit test,不关心method的依赖。比如我有一个MethodA,其实现依赖于接口B和C,其中C又依赖接口D。在第一种方案中,该unit test需要执行完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拦截其真实执行,返回一个预先设置的值,从而达到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. }  




    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多