Spring專案單元測試
阿新 • • 發佈:2019-02-02
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實現,常見實現程式碼如下。spring配置檔案通過@ContextConfiguration注入,事務通過@TransactionConfiguration宣告。如果還有一些listener,可以通過@TestExecutionListeners方式注入。基本上可以滿足測試需求。 簡單的action示例,service同理。@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{ //... 公用程式碼部分 }
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());
}
}