[本文出自天外歸雲的博客園]
概要簡述
利用JUnit結合Mockito,再加上spingframework自帶的一些方法,就可以組合起來對Spring MVC中的Controller層進行測試。
在設計測試用例前,我們要對待測Controller的代碼邏輯進行逐層深入的走查。走查的目的是要明確Controller中主要邏輯分支,以便設計測試用例進行覆蓋。一些主要通用的關注點有:
1. 請求request中所包含的參數值(Controller中從請求中獲取的參數)
2. Controller中的try塊中能夠引起異常的方法調用
3. Controller中的if語句涉及的變量值
4. 一些ThreadLocal方法(在實際測試過程中需要對ThreadLocal對象做一些操作來模擬一些狀態)
測試規范
創建后端測試分支:一定是以開發分支為基礎創建,也為通過修改開發代碼來調試測試代碼創造方便。
創建測試類:測試類名B與待測類名A的關系為B=ATest
測試類上添加注釋:@RunWith(MockitoJUnitRunner.class)
測試類中聲明:private MockMvc mockMvc;
測試類中待注入mock的對象聲明上添加注釋:@InjectMocks
測試類中待mock的對象聲明上添加注釋:@Mock
測試方法上添加注釋:@Test
常用引用(如果IDE不能自動下載對應maven倉庫,則需手動修改pom.xml文件添加引用相應的maven倉庫):
import org.junit.Before; import org.junit.Test; import org.junit.runner.JUnitCore; import org.junit.runner.Result; import org.junit.runner.RunWith; import org.junit.runner.notification.Failure; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
初始化方法標准范例:
@Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); this.mockMvc = MockMvcBuilders.standaloneSetup(聲明的controller).build(); }
測試方法標准結構范例:
@Test public void testSth() throws Exception { // 自定義填充 // Mock依賴 // 構造請求 // 執行請求與斷言 }
對環境ThreadLocal的填充方法舉例:
//填充為空 XXThreadLocalContainer.XXX_THREAD_LOCAL.set(null); //填充為指定類型對象 A a= new A(); a.setXX("test"); XXThreadLocalContainer.XXX_THREAD_LOCAL.set(a);
對無返回service方法依賴注入的mock方法舉例:
doThrow(Exception.class).when(someService).doSomeMethod(any(SomeClassA.class), any(SomeClassB.class), anyString(), anyString(), any(SomeEnum.class));
對有返回service方法依賴注入的mock方法舉例:
// 構造mock方法返回的對象 A a = new A(); a.setSomePropertyA(someValueA); a.setSomePropertyB(someValueB); // 構造mock方法 doReturn(a).when(someService).doSomeMethod(anyString(), anyString());
利用RequestBuilder構造GET請求舉例:
RequestBuilder request = MockMvcRequestBuilders.get(someUrl).requestAttr("someAttrNameA", someValueA).requestAttr("someAttrNameB", someValueB).requestAttr("someAttrNameC", someValueC);
執行請求與斷言舉例:
mockMvc.perform(request).andDo(print()).andExpect(jsonPath("someFieldName").value(String.valueOf(someFieldValue)));
自定義main函數執行測試:
public static void main(String[] args) { Result result = JUnitCore.runClasses(MpResurrectionControllerTest.class); for (Failure failure : result.getFailures()) { System.out.println(String.format("FAILED : %s", failure.toString())); } System.out.println(String.format("TEST SUCCESS : %s", result.wasSuccessful())); }
抽象復用
在實際的測試過程中,把復用的部分提取抽象,生成一個基類為測試類提供繼承(MockTestBase.java),其中testAll方法利用反射通過類名動態生成類對象:
package com.xx.xxx; import org.junit.Before; import org.junit.Rule; import org.junit.rules.ExpectedException; import org.junit.runner.JUnitCore; import org.junit.runner.Result; import org.junit.runner.RunWith; import org.junit.runner.notification.Failure; import org.mockito.MockitoAnnotations; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.test.web.servlet.MockMvc; /** * @Author: Tylan * @CreateDate: 2018/7/2 11:07 * @UpdateDate: 2018/7/2 11:07 */ @RunWith(MockitoJUnitRunner.class) public abstract class MockTestBase { MockMvc mockMvc; @Rule public ExpectedException thrown = ExpectedException.none(); @Before public void setUp() { MockitoAnnotations.initMocks(this); } public static void testAll(String className) throws ClassNotFoundException { Class obj = Class.forName(className); Result result = JUnitCore.runClasses(obj); for (Failure failure : result.getFailures()) { System.out.println(String.format("FAILED : %s", failure.toString())); } System.out.println(String.format("TEST SUCCESS : %s", result.wasSuccessful())); } }
新的測試類就變成這樣,重新寫一個main函數調用基類testAll方法,利用反射傳入當前運行的類名:
package com.xx.xxx; import com.xx.xxx.ForTestController; import com.xx.xxx.SomeService; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; /** * @Author: Tylan * @CreateDate: 2018/7/2 11:11 * @UpdateDate: 2018/7/2 11:11 */ public class NewMpGameOrderControllerTestsTylan extends MockTestBase { @InjectMocks private ForTestController someController; @Mock SomeService someService; @Override public void setUp() { this.mockMvc = MockMvcBuilders.standaloneSetup(someController).build(); } @Test public void insertTestWrongParam() throws Exception { /* * 測試data為空 * */ System.out.println("insertTestWrongParam"); // 構造測試數據 String data1 = ""; String data2 = ""; String insertUrl = "/xx/xx/data/insert"; // 執行請求與斷言 RequestBuilder request = MockMvcRequestBuilders.get(insertUrl).requestAttr("data1", data1).requestAttr("data2", data2); mockMvc.perform(request).andDo(print()).andExpect(jsonPath("xxx").value("xx")); } public static void main(String[] args) throws ClassNotFoundException { testAll(Thread.currentThread().getStackTrace()[1].getClassName()); } }
這就生成了一個測試Controller的模板。剩下的工作就是分析開發源碼,設計測試用例並對Controller中的所有邏輯分支進行覆蓋測試了。
在一個單元測試用例,標准順序是mock、test、verify三個環節。其中mock舉例:
GameOrder order = new GameOrder(); order.setId(xxx); order.setScore(xxx); doReturn(order).when(gameService).initGameOrder(anyString(), anyString(), anyString(), any(MpEnum.class));
涉及對foreach循環的mock舉例:
Iterator<SomeService> mockIter = mock(Iterator.class); when(mockIter.hasNext()).thenReturn(true, true, true, false); when(mockIter.next()).thenReturn(someService).thenReturn(someService).thenReturn(someService); when(serviceList.iterator()).thenReturn(mockIter); doReturn(SomeEnum.XXXXX).when(someService).getSomeEnum(); doReturn(true).when(someService).isValid(anyMap());
對於test環節,測試Controller就是模擬給Controller發請求,測試Service就是直接調用Service方法。
對於Verify環節,主要是對一些方法是否執行,路徑是否走過做一下驗證,以及一些值的驗證。這里舉個例子:
//Verify verify(someService).initGameOrder(xx, xxx, xxxx, someEnum); verify(someService).update(any(GameOrder.class));
這里驗證了someService對象是否執行了initGameOrder方法和update方法。
感受與總結
想做好單元測試,要對待測試代碼邏輯進行充分分析,重點邏輯是在Service層和Controller層的測試,而對於這兩層來說,Controller中除了一些基本邏輯基本就是service層的調用,對於service層,除了定義service的interface類就是定義對應implements的接口實現類,interface類只定義方法接口,剩余的由service的impl類實現,service實現類中除了Override實現繼承的接口類定義的方法接口就是Autowired聲明一些在該實現類中要用到的其他service接口類(非實現類),剩下的就是接口類一層又一層的繼承關系,而service層無非是圍繞着dao層展開的,最終落在了dao層對數據庫或緩存的操作上。所以縱觀整個后端結構順序就是Filter層-Controller層-Service層-Dao層,請求是按這個順序前進與原路返回的。所以后端測試的重點,最終是落在Controller層邏輯的正確性與sql查詢、redis和memcache等緩存查詢的正確性上。
單測是否應該由開發人員進行?這個問題的答案和開發是否應該由測試人員完成是一樣的,如果讓開發搞測試沒什么不可以,那么讓測試做開發也能更好的保障質量。但是現實的分工中,沒有那么多全棧人才,測試人員不懂開發,開發人員不懂測試。單測是應該由測試人員完成的,想要保障產品質量,如果QA都對代碼邏輯沒有了解的話,單憑瞎子摸象這種經驗推測式的方法進行測試,能夠發現的問題種類和層次也是有限的。而開發除了緊張的開發周期外,還要修改bug,沒有更多的時間來投入到測試中。所以測試人員應該具備能夠發現bug和定位問題原因的能力才能夠更好的配合開發一起完成高質量的產品軟件工程。