1. 程式人生 > >Spring專案單元測試

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());
	}
}