Spring项目单元测试

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实现,常见实现代码如下。
@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());
	}
}




阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页