一、前言 编写单元测试可以帮助开发人员编写高质量的代码,提升代码质量,减少Bug,便于重构。Spring Boot提供了一些实用程序和注解,用来帮助我们测试应用程序,在Spring Boot中开启单元测试只需引入spring-boot-starter-test即可,其包含了一些主流的测试库。本文主要介绍基于 Service和Controller的单元测试。
二、引入spring-boot-starter-test 在spring项目创建时,默认是引入了spring-boot-starter-test,但实际进行单元测试时是需要引入junit。引入具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency>
或者直接引入
1 2 3 4 5 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
三、知识准备: 3.1 Unit4注解 JUnit4中包含了几个比较重要的注解:@BeforeClass、@AfterClass、@Before、@After和@Test。其中, @BeforeClass和@AfterClass在每个类加载的开始和结束时运行,必须为静态方法;而@Before和@After则在每个测试方法开始之前和结束之后运行。见如下例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import org.junit.*; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class TestApplicationTests { @BeforeClass public static void beforeClassTest() { System.out.println("before class test"); } @Before public void beforeTest() { System.out.println("before test"); } @Test public void Test1() { System.out.println("test 1+1=2"); Assert.assertEquals(2, 1 + 1); } @Test public void Test2() { System.out.println("test 2+2=4"); Assert.assertEquals(4, 2 + 2); } @After public void afterTest() { System.out.println("after test"); } @AfterClass public static void afterClassTest() { System.out.println("after class test"); } }
运行输出如下:
1 2 3 4 5 6 7 8 9 10 ... before class test before test test 1+1=2 after test before test test 2+2=4 after test after class test ...
从上面的输出可以看出各个注解的运行时机。注意
:引入头信息必须按照上面的进行加载,否则会造成@After等不能生效。
3.2 Assert 上面代码中,我们使用了Assert类提供的assert口方法,下面列出了一些常用的assert方法:
assertEquals("message",A,B),判断A对象和B对象是否相等,这个判断在比较两个对象时调用了equals()方法。 assertSame("message",A,B),判断A对象与B对象是否相同,使用的是==操作符。 assertTrue("message",A),判断A条件是否为真。 assertFalse("message",A),判断A条件是否不为真。 assertNotNull("message",A),判断A对象是否不为null。 assertArrayEquals("message",A,B),判断A数组与B数组是否相等。
3.3 MockMvc 下文中,对Controller的测试需要用到MockMvc技术。MockMvc,从字面上来看指的是模拟的MVC,即其可以模拟一个MVC环境,向Controller发送请求然后得到响应。
在单元测试中,使用MockMvc前需要进行初始化,如下所示:
1 2 3 4 5 6 7 8 9 private MockMvc mockMvc; @Autowired private WebApplicationContext wac; @Before public void setupMockMvc(){ mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); }
MockMvc模拟MVC请求
模拟一个get请求:
mockMvc.perform(MockMvcRequestBuilders.get("/hello?name={name}","mrbird"));
模拟一个post请求:
mockMvc.perform(MockMvcRequestBuilders.post("/user/{id}", 1));
模拟文件上传:
mockMvc.perform(MockMvcRequestBuilders.fileUpload("/fileupload").file("file", "文件内容".getBytes("utf-8")));
模拟请求参数:
1 2 3 4 // 模拟发送一个message参数,值为hello mockMvc.perform(MockMvcRequestBuilders.get("/hello").param("message", "hello")); // 模拟提交一个checkbox值,name为hobby,值为sleep和eat mockMvc.perform(MockMvcRequestBuilders.get("/saveHobby").param("hobby", "sleep", "eat"));
也可以直接使用MultiValueMap构建参数:
1 2 3 4 5 MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>(); params.add("name", "mrbird"); params.add("hobby", "sleep"); params.add("hobby", "eat"); mockMvc.perform(MockMvcRequestBuilders.get("/hobby/save").params(params));
模拟发送JSON参数:
1 2 String jsonStr = "{\"username\":\"Dopa\",\"passwd\":\"ac3af72d9f95161a502fd326865c2f15\",\"status\":\"1\"}"; mockMvc.perform(MockMvcRequestBuilders.post("/user/save").content(jsonStr.getBytes()));
实际测试中,要手动编写这么长的JSON格式字符串很繁琐也很容易出错,可以借助Spring Boot自带的Jackson技术来序列化一个Java对象(可参考Spring Boot中的JSON技术),如下所示:
1 2 3 4 5 6 7 User user = new User(); user.setUsername("Dopa"); user.setPasswd("ac3af72d9f95161a502fd326865c2f15"); user.setStatus("1"); String userJson = mapper.writeValueAsString(user); mockMvc.perform(MockMvcRequestBuilders.post("/user/save").content(userJson.getBytes()));
其中,mapper为com.fasterxml.jackson.databind.ObjectMapper对象。
模拟Session和Cookie:
1 2 mockMvc.perform(MockMvcRequestBuilders.get("/index").sessionAttr(name, value)); mockMvc.perform(MockMvcRequestBuilders.get("/index").cookie(new Cookie(name, value)));
设置请求的Content-Type:
mockMvc.perform(MockMvcRequestBuilders.get("/index").contentType(MediaType.APPLICATION_JSON_UTF8));
设置返回格式为JSON:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1).accept(MediaType.APPLICATION_JSON));
模拟HTTP请求头:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1).header(name, values));
MockMvc处理返回结果
期望成功调用,即HTTP Status为200:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1)) .andExpect(MockMvcResultMatchers.status().isOk());
期望返回内容是application/json:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1)) .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON));
检查返回JSON数据中某个值的内容:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1)) .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("mrbird"));
这里使用到了jsonPath,$代表了JSON的根节点。更多关于jsonPath的介绍可参考 https://github.com/json-path/JsonPath。
判断Controller方法是否返回某视图:
mockMvc.perform(MockMvcRequestBuilders.post("/index")) .andExpect(MockMvcResultMatchers.view().name("index.html"));
比较Model:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1)) .andExpect(MockMvcResultMatchers.model().size(1)) .andExpect(MockMvcResultMatchers.model().attributeExists("password")) .andExpect(MockMvcResultMatchers.model().attribute("username", "mrbird"));
比较forward或者redirect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 mockMvc.perform(MockMvcRequestBuilders.get("/index")).andExpect(MockMvcResultMatchers.forwardedUrl("index.html")); // 或者 mockMvc.perform(MockMvcRequestBuilders.get("/index")).andExpect(MockMvcResultMatchers.redirectedUrl("index.html")); 比较返回内容,使用content(): // 返回内容为hello mockMvc.perform(MockMvcRequestBuilders.get("/index")).andExpect(MockMvcResultMatchers.content().string("hello")); // 返回内容是XML,并且与xmlCotent一样 mockMvc.perform(MockMvcRequestBuilders.get("/index")) .andExpect(MockMvcResultMatchers.content().xml(xmlContent)); // 返回内容是JSON ,并且与jsonContent一样 mockMvc.perform(MockMvcRequestBuilders.get("/index")) .andExpect(MockMvcResultMatchers.content().json(jsonContent));
输出响应结果:
mockMvc.perform(MockMvcRequestBuilders.get("/index")) .andDo(MockMvcResultHandlers.print());
四、测试Service 具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 @RunWith(SpringRunner.class) @SpringBootTest public class UserServiceTest { @Autowired UserService userService; @Test public void test() { User user = this.userService.findByName("scott"); Assert.assertEquals("用户名为scott", "scott", user.getUsername()); } }
运行后,JUnit没有报错说明测试通过,即UserService的findByName方法可行。
此外,和在Controller中引用Service相比,在测试单元中对Service测试完毕后,数据能自动回滚,只需要在测试方法上加上@Transactional注解,比如:
1 2 3 4 5 6 7 8 9 10 11 @Test @Transactional public void test() { User user = new User(); user.setId(this.userService.getSequence("seq_user")); user.setUsername("JUnit"); user.setPasswd("123456"); user.setStatus("1"); user.setCreateTime(new Date()); this.userService.save(user); }
运行,测试通过,查看数据库发现数据并没有被插入,这样很好的避免了不必要的数据污染。
五、测试Controller 现有如下Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController public class UserController { @Autowired UserService userService; @GetMapping("user/{userName}") public User getUserByName(@PathVariable(value = "userName") String userName) { return this.userService.findByName(userName); } @PostMapping("user/save") public void saveUser(@RequestBody User user) { this.userService.saveUser(user); } }
现在编写一个针对于该ControllergetUserByName(@PathVariable(value = "userName") String userName)方法的测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @RunWith(SpringRunner.class) @SpringBootTest public class UserControllerTest { private MockMvc mockMvc; @Autowired private WebApplicationContext wac; @Before public void setupMockMvc(){ mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } @Test public void test() throws Exception { mockMvc.perform( MockMvcRequestBuilders.get("/user/{userName}", "scott") .contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("scott")) .andDo(MockMvcResultHandlers.print()); } }
运行后,JUnit通过,控制台输出过程如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 MockHttpServletRequest: HTTP Method = GET Request URI = /user/scott Parameters = {} Headers = {Content-Type=[application/json;charset=UTF-8]} Handler: Type = demo.springboot.test.controller.UserController Method = public demo.springboot.test.domain.User demo.springboot.test.controller.UserController.getUserByName(java.lang.String) Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[application/json;charset=UTF-8]} Content type = application/json;charset=UTF-8 Body = {"id":23,"username":"scott","passwd":"ac3af72d9f95161a502fd326865c2f15","createTime":1514535399000,"status":"1"} Forwarded URL = null Redirected URL = null Cookies = []
继续编写一个针对于该ControllersaveUser(@RequestBody User user)方法的测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @RunWith(SpringRunner.class) @SpringBootTest public class UserControllerTest { private MockMvc mockMvc; @Autowired private WebApplicationContext wac; @Autowired ObjectMapper mapper; @Before public void setupMockMvc(){ mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } @Test @Transactional public void test() throws Exception { User user = new User(); user.setUsername("Dopa"); user.setPasswd("ac3af72d9f95161a502fd326865c2f15"); user.setStatus("1"); String userJson = mapper.writeValueAsString(user); mockMvc.perform( MockMvcRequestBuilders.post("/user/save") .contentType(MediaType.APPLICATION_JSON_UTF8) .content(userJson.getBytes())) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); } }
运行过程如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 MockHttpServletRequest: HTTP Method = POST Request URI = /user/save Parameters = {} Headers = {Content-Type=[application/json;charset=UTF-8]} Handler: Type = demo.springboot.test.controller.UserController Method = public void demo.springboot.test.controller.UserController.saveUser(demo.springboot.test.domain.User) Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = {} Content type = null Body = Forwarded URL = null Redirected URL = null Cookies = []
值得注意的是,在一个完整的系统中编写测试单元时,可能需要模拟一个登录用户信息Session,MockMvc也提供了解决方案,可在初始化的时候模拟一个HttpSession:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private MockMvc mockMvc; private MockHttpSession session; @Autowired private WebApplicationContext wac; @Before public void setupMockMvc(){ mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); session = new MockHttpSession(); User user =new User(); user.setUsername("Dopa"); user.setPasswd("ac3af72d9f95161a502fd326865c2f15"); session.setAttribute("user", user); }