Unit test与developing之间的矛盾由来已久,unit test带来的时间成本是否能超过其对质量的提升,每个团队的结果都不相同。比如团结成熟度很高,那么一些简单的unit test或许带来不了什么收益;但是如果团队比较年轻,成员也有很多经验不够丰富的开发人员,不可避免会有一些低级bug出现,unit test的收益就会相对明显。做不做都是这个团队的取舍。
本文针对Spring项目的unit test提出几种方案,并加以分析。Spring project的核心是bean,所以unit test不可避免需要能够生产“bean”,因此有两种实现方式:
- 加载spring配置,类似项目容器加载
- mock spring bean,对bean的调用方法进行拦截
依赖spring bean的unit test测试方案
这种方案的还原度最高,与真实运行的差别仅仅是容器,服务器环境等因素。常见的实现方案通过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)。
查阅资料发现有一种方案是采用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),想要打印两串字符,一串数字和一串字母。
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());
}
}