單元測試的方法有很多種,比如使用Postman、SoapUI等工具測試,當然,這里的測試,主要使用的是基於RESTful風格的SpringMVC的測試,我們可以測試完整的Spring MVC流程,即從URL請求到控制器處理,再到視圖渲染都可以測試。下面我主要總結下Spring Boot基於Mock的方式對控制層Controller和服務層Serivce的單元測試。盡管這種的文章已經有很多,我的總結只是作為自己學習的一個承載,總結有誤的地方歡迎小伙伴們指正,同時也希望能幫助跟我一樣還在學習的小伙伴們。
在介紹Mock API測試方法之前先介紹下Spring MVC測試框架提供的兩種方式:
獨立安裝測試和集成Web環境測試(此種方式並不會集成真正的web環境,而是通過相應的Mock API進行模擬測試,無須啟動服務器)。
MockMvcBuilder介紹:
是用來構造MockMvc的構造器,其主要有兩個實現:StandaloneMockMvcBuilder和DefaultMockMvcBuilder,StandaloneMockMvcBuilder繼承了DefaultMockMvcBuilder。直接使用靜態工廠MockMvcBuilders創建即可:
1. 集成Web環境測試:
MockMvcBuilders.webAppContextSetup(WebApplicationContext context):指定WebApplicationContext,將會從該上下文獲取相應的控制器並得到相應的MockMvc;
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes=MockServletContext.class) @WebAppConfiguration public class StudentControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); // 構造MockMvc } // ... }
注意:
(1)@WebAppConfiguration:測試環境使用,用來表示測試環境使用的ApplicationContext將是WebApplicationContext類型的;value指定web應用的根;
(2)通過@Autowired WebApplicationContext wac:注入web環境的ApplicationContext容器;
(3)然后通過MockMvcBuilders.webAppContextSetup(wac).build()創建一個MockMvc進行測試;
2.獨立測試方式
MockMvcBuilders.standaloneSetup(Object... controllers):通過參數指定一組控制器,這樣就不需要從上下文獲取了;
(SpringMVC測試需要添加:mockito-core和mockito-all依賴)
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"classpath*:/spring-context.xml"}) public class DemoTest { @Mock private StudentService studentService; @InjectMocks private StudentController studentController; private MockMvc mockMvc; @Before public void setup() { MockitoAnnotations.initMocks(this); this.mockMvc = MockMvcBuilders.standaloneSetup(bookController).build(); } @Test public void testXX() throws Exception { //... } }
主要是兩個步驟:
(1)首先自己創建相應的控制器,注入相應的依賴
(2)通過MockMvcBuilders.standaloneSetup模擬一個Mvc測試環境,通過build得到一個MockMvc。
perform:執行一個RequestBuilder請求,會自動執行SpringMVC的流程並映射到相應的控制器執行處理;
andExpect:添加ResultMatcher驗證規則,驗證控制器執行完成后結果是否正確;
andDo:添加ResultHandler結果處理器,比如調試時打印結果到控制台;
andReturn:最后返回相應的MvcResult;然后進行自定義驗證/進行下一步的異步處理;
MockMvcRequestBuilders主要API:
MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables):根據uri模板和uri變量值得到一個GET請求方式的MockHttpServletRequestBuilder;如get("/user/{id}", 1L);
MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables):同get類似,但是是POST方法;
MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables):同get類似,但是是PUT方法;
MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables) :同get類似,但是是DELETE方法;
MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables):同get類似,但是是OPTIONS方法;
Mock測試過程:
1、mockMvc.perform執行一個請求(MockMvcRequestBuilders.get("/user/1")構造一個請求).
2、設置參數(這一步其實可以設置很多參數,MockMvc提供了豐富的方法)
3、mockMvc調用perform,調用controller的業務處理邏輯
4、perform返回ResultActions,返回操作結果,通過ResultActions,提供了統一的驗證方式。(
ResultActions.andExpect添加執行完成后的斷言,
ResultActions.andDo添加一個結果處理器,表示要對結果做點什么事情,比如此處使用MockMvcResultHandlers.print()輸出整個響應結果信息。
ResultActions.andReturn表示執行完成后返回相應的結果。
測試說明:
1. 依賴包導入:pom.xml中僅依賴spring-boot-starter-test,它把會把測試相關的依賴全部引入。
2. 在測試類上的注解,常用的注解有三個:
@RunWith(SpringJUnit4ClassRunner.class)//引用Spring-Test測試框架支持
@SpringApplicationConfiguration(classes = StartApp.class) // StartApp :可以是Spring Boot的啟動類,也可以使用MockServletContext來構建一個空的WebApplicationContext,這樣我們創建的StudentController就可以在@Before函數中創建並傳遞到MockMvcBuilders.standaloneSetup()函數中。
@WebAppConfiguration //用來表示測試環境使用的ApplicationContext將是WebApplicationContext類型的
3. 測試類的文件結構,保持src/test/java和src/main/java結構一致,即:包+文件夾。測試配置文件任然用src/main/resources的。
根據如上介紹看下如下案例:
定義Student實體類:

public class Student implements Serializable{ /** * */ private static final long serialVersionUID = -9143765513634702342L; private Long id; private String name; private String age; private String address; public String getName() { return name; } //省略get/set方法... }
定義IStudentService接口類:

public interface IStudentService { public List<Student> getStudentList(); public int addStudent(Student student); public Student getStudent(Long id); public int updateStudent(Student student); public int deleteStudent(Long id); }
定義IStudentService接口類實現:

@Service public class StudentService implements IStudentService { /** * 定義一個線程安全的集合(集合驗證時獲取不到存的值,不影響學習mock測試方法) */ static Map<Long, Student> students = Collections.synchronizedMap(new HashMap<Long, Student>()); @Override public List<Student> getStudentList() { List<Student> stuList = new ArrayList<Student>(); stuList.addAll(students.values()); return stuList; } @Override public int addStudent(Student student) { students.put(student.getId(), student); return 0; } @Override public Student getStudent(Long id) { Student stu = students.get(id); return stu; } @Override public int updateStudent(Student student) { Long id = student.getId(); Student stu = students.get(id); stu.setAddress(student.getAddress()); stu.setAge(student.getAge()); stu.setName(student.getName()); students.put(id, stu); return 0; } @Override public int deleteStudent(Long id) { students.remove(id); return 0; } }
定義Controller類:

@RestController @RequestMapping(value="/stu") public class StudentController { @Autowired private IStudentService iStudentService; @RequestMapping(value="/",method=RequestMethod.GET) @ResponseBody public List<Student> getStudentList(){ List<Student> list = iStudentService.getStudentList(); System.out.println("==="+list.size()); return list; } @RequestMapping(value="/{id}",method=RequestMethod.GET) @ResponseBody public Student getStudent(@PathVariable(value="id")Long id){ return iStudentService.getStudent(id); } @RequestMapping(value = "/", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @ResponseBody public String addStudent(@ModelAttribute Student student) { iStudentService.addStudent(student); return "success"; } @RequestMapping(value = "/", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @ResponseBody public String updateStudent(@ModelAttribute Student student) { iStudentService.addStudent(student); return "success"; } @RequestMapping(value="/{id}",method=RequestMethod.DELETE) public String deleteStudent(@PathVariable Long id){ iStudentService.deleteStudent(id); return "success"; } }
定義Controller測試類:

@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes=MockServletContext.class) @WebAppConfiguration public class StudentControllerTest { private static final Log log = LogFactory.getLog(StudentApplicationTest.class); // @Autowired // private WebApplicationContext context; @Mock private IStudentService iStudentService; //@Mock: 創建一個Mock. //@InjectMocks: 創建一個實例,其余用@Mock(或@Spy)注解創建的mock將被注入到用該實例中。 @InjectMocks private StudentController studentController; private MockMvc mvc; private final String json = "[{\"id\":1,\"name\":\"xiao\",\"age\":27,\"address\":\"湖北\"}]"; @Before public void setUp(){ MockitoAnnotations.initMocks(this); mvc = MockMvcBuilders.standaloneSetup(studentController).build();////設置要mock的Controller類 //mvc = MockMvcBuilders.webAppContextSetup(context).build();//建議使用這種 } @Test public void testStudentController() throws Exception { log.info("開始測試================="); log.info("測試新增學生==================="); RequestBuilder request = post("/stu/").param("id", "1") .param("name", "xiao") .param("age", "27") .param("address","湖北"); mvc.perform(request).andExpect(content().string(equalTo("success"))).andDo(print()); log.info("測試獲取學生============"); request = get("/stu/"); mvc.perform(request) .andExpect(status().isOk()) .andExpect(content().string(equalTo(json))); } }
定義Service測試類:

@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes=Application.class) @WebAppConfiguration public class StudentServiceTest { @Autowired private IStudentService iStudentSerivce; @Test public void testAddStudent(){ Student stu = new Student(); stu.setId(1l); stu.setAge("27"); stu.setName("xiaoming"); stu.setAddress("China"); int result = iStudentSerivce.addStudent(stu); Assert.assertEquals(0, result); } @Test public void testgetStudent(){ Student stu = iStudentSerivce.getStudent(1l); Assert.assertEquals(stu.getName(), "xiaohua"); } }
附單元測試相關知識:
Junit單元測試:
//所有測試方法執行前.執行一次,作用:整體初始化 @BeforeClass //所有測試方法完成后,執行一次,作用:銷毀和釋放資源 @AfterClass //每個測試方法前執行,作用:初始化方法 @Before //每個測試方法后執行,作用:還原現場 @After // 測試方法超過1000毫秒,記為超時,測試失敗 @Test(timeout = 1000) // 測試方法期望得到的異常類,如果方法執行沒有拋出指定的異常,則測試失敗 @Test(expected = Exception.class) // 執行測試時將忽略掉此方法,如果用於修飾類,則忽略整個類 @Ignore(“not ready yet”) @Test @RunWith 在JUnit中有很多個Runner,他們負責調用你的測試代碼,每一個Runner都有各自的特殊功能,你要根據需要選擇不同的Runner來運行你的測試代碼。 如果我們只是簡單的做普通Java測試,不涉及spring Web項目,你可以省略@RunWith注解,這樣系統會自動使用默認Runner來運行你的代碼。
Assert斷言方法介紹:
1、assertEquals 函數原型1:assertEquals([String message],expected,actual) 參數說明: message是個可選的消息,假如提供,將會在發生錯誤時報告這個消息。 expected是期望值,通常都是用戶指定的內容。 actual是被測試的代碼返回的實際值。 例:assertEquals("equals","1","1"); 函數原型2:assertEquals([String message],expected,actual,tolerance) 參數說明: message是個可選的消息,假如提供,將會在發生錯誤時報告這個消息。 expected是期望值,通常都是用戶指定的內容。 actual是被測試的代碼返回的實際值。 tolerance是誤差參數,參加比較的兩個浮點數在這個誤差之內則會被認為是 相等的。 例:assertEquals ("yes",5.8,11.0/2.0,0.5); 2、assertTrue 函數原型:assertTrue ([String message],Boolean condition) 參數說明: message是個可選的消息,假如提供,將會在發生錯誤時報告這個消息。 condition是待驗證的布爾型值。 該斷言用來驗證給定的布爾型值是否為真,假如結果為假,則驗證失敗。當然,更有驗證為假的測試條件: 函數原型:assertFalse([String message],Boolean condition) 該斷言用來驗證給定的布爾型值是否為假,假如結果為真,則驗證失敗。 例: assertTrue("true",1==1); assertFalse("false",2==1); 3、assertNull 函數原型:assertNull([String message],Object object) 參數說明: message是個可選的消息,假如提供,將會在發生錯誤時報告這個消息。 object是待驗證的對象。 該斷言用來驗證給定的對象是否為null,假如不為null,則驗證失敗。相應地,還存在能夠驗證非null的斷言: 函數原型:assertNotNull([String message],Object object) 該斷言用來驗證給定的對象是否為非null,假如為null,則驗證失敗。 例:assertNull("null",null); assertNotNull("not null",new String()); 4、assertSame 函數原型:assertSame ([String message], expected,actual) 參數說明: message是個可選的消息,假如提供,將會在發生錯誤時報告這個消息。 expected是期望值。 actual是被測試的代碼返回的實際值。 該斷言用來驗證expected參數和actual參數所引用的是否是同一個對象,假如不是,則驗證失敗。相應地,也存在驗證不是同一個對象的斷言: 函數原型:assertNotSame ([String message], expected,actual) 該斷言用來驗證expected參數和actual參數所引用的是否是不同對象,假如所引用的對象相同,則驗證失敗。 例:assertSame("same",2,4-2); assertNotSame("not same",2,4-3); 5、Fail 函數原型:Fail([String message]) 參數說明: message是個可選的消息,假如提供,將會在發生錯誤時報告這個消息。 該斷言會使測試立即失敗,通常用在測試不能達到的分支上(如異常)。 注:新版的Junit中,assertEquals 方法已經被廢棄,它建議我們使用assertArrayEquals,旨在讓我們測試一個方法的時候多傳幾種參數進行多種可能性測試。