Spring Boot中编写单元测试 一、前言编写单元测试可以帮助开发人员编写高质量的代码提升代码质量减少Bug便于重构。Spring Boot提供了一些实用程序和注解用来帮助我们测试应用程序在Spring Boot中开启单元测试只需引入spring-boot-starter-test即可其包含了一些主流的测试库。本文主要介绍基于 Service和Controller的单元测试。二、引入spring-boot-starter-test在spring项目创建时默认是引入了spring-boot-starter-test但实际进行单元测试时是需要引入junit。引入具体如下dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scopeexclusionsexclusiongroupIdorg.junit.vintage/groupIdartifactIdjunit-vintage-engine/artifactId/exclusion/exclusions/dependencydependencygroupIdjunit/groupIdartifactIdjunit/artifactIdscopetest/scope/dependency或者直接引入dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency三、知识准备3.1 Unit4注解JUnit4中包含了几个比较重要的注解BeforeClass、AfterClass、Before、After和Test。其中 BeforeClass和AfterClass在每个类加载的开始和结束时运行必须为静态方法而Before和After则在每个测试方法开始之前和结束之后运行。见如下例子importorg.junit.*;importorg.junit.runner.RunWith;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.test.context.junit4.SpringRunner;RunWith(SpringRunner.class)SpringBootTestpublicclassTestApplicationTests{BeforeClasspublicstaticvoidbeforeClassTest(){System.out.println(before class test);}BeforepublicvoidbeforeTest(){System.out.println(before test);}TestpublicvoidTest1(){System.out.println(test 112);Assert.assertEquals(2,11);}TestpublicvoidTest2(){System.out.println(test 224);Assert.assertEquals(4,22);}AfterpublicvoidafterTest(){System.out.println(after test);}AfterClasspublicstaticvoidafterClassTest(){System.out.println(after class test);}}运行输出如下... before class test before test test 112 after test before test test 224 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前需要进行初始化如下所示privateMockMvcmockMvc;AutowiredprivateWebApplicationContextwac;BeforepublicvoidsetupMockMvc(){mockMvcMockMvcBuilders.webAppContextSetup(wac).build();}MockMvc模拟MVC请求模拟一个get请求mockMvc.perform(MockMvcRequestBuilders.get(/hello?name{name},wno704));模拟一个post请求mockMvc.perform(MockMvcRequestBuilders.post(/user/{id},1));模拟文件上传mockMvc.perform(MockMvcRequestBuilders.fileUpload(/fileupload).file(file,文件内容.getBytes(utf-8)));模拟请求参数// 模拟发送一个message参数值为hellomockMvc.perform(MockMvcRequestBuilders.get(/hello).param(message,hello));// 模拟提交一个checkbox值name为hobby值为sleep和eatmockMvc.perform(MockMvcRequestBuilders.get(/saveHobby).param(hobby,sleep,eat));也可以直接使用MultiValueMap构建参数MultiValueMapString,StringparamsnewLinkedMultiValueMapString,String();params.add(name,wno704);params.add(hobby,sleep);params.add(hobby,eat);mockMvc.perform(MockMvcRequestBuilders.get(/hobby/save).params(params));模拟发送JSON参数StringjsonStr{\username\:\Dopa\,\passwd\:\ac3af72d9f95161a502fd326865c2f15\,\status\:\1\};mockMvc.perform(MockMvcRequestBuilders.post(/user/save).content(jsonStr.getBytes()));实际测试中要手动编写这么长的JSON格式字符串很繁琐也很容易出错可以借助Spring Boot自带的Jackson技术来序列化一个Java对象可参考Spring Boot中的JSON技术如下所示UserusernewUser();user.setUsername(Dopa);user.setPasswd(ac3af72d9f95161a502fd326865c2f15);user.setStatus(1);StringuserJsonmapper.writeValueAsString(user);mockMvc.perform(MockMvcRequestBuilders.post(/user/save).content(userJson.getBytes()));其中mapper为com.fasterxml.jackson.databind.ObjectMapper对象。模拟Session和CookiemockMvc.perform(MockMvcRequestBuilders.get(/index).sessionAttr(name,value));mockMvc.perform(MockMvcRequestBuilders.get(/index).cookie(newCookie(name,value)));设置请求的Content-TypemockMvc.perform(MockMvcRequestBuilders.get(/index).contentType(MediaType.APPLICATION_JSON_UTF8));设置返回格式为JSONmockMvc.perform(MockMvcRequestBuilders.get(/user/{id},1).accept(MediaType.APPLICATION_JSON));模拟HTTP请求头mockMvc.perform(MockMvcRequestBuilders.get(/user/{id},1).header(name,values));MockMvc处理返回结果期望成功调用即HTTP Status为200mockMvc.perform(MockMvcRequestBuilders.get(/user/{id},1)).andExpect(MockMvcResultMatchers.status().isOk());期望返回内容是application/jsonmockMvc.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(wno704));这里使用到了jsonPath$代表了JSON的根节点。更多关于jsonPath的介绍可参考 https://github.com/json-path/JsonPath。判断Controller方法是否返回某视图mockMvc.perform(MockMvcRequestBuilders.post(/index)).andExpect(MockMvcResultMatchers.view().name(index.html));比较ModelmockMvc.perform(MockMvcRequestBuilders.get(/user/{id},1)).andExpect(MockMvcResultMatchers.model().size(1)).andExpect(MockMvcResultMatchers.model().attributeExists(password)).andExpect(MockMvcResultMatchers.model().attribute(username,wno704));比较forward或者redirectmockMvc.perform(MockMvcRequestBuilders.get(/index)).andExpect(MockMvcResultMatchers.forwardedUrl(index.html));// 或者mockMvc.perform(MockMvcRequestBuilders.get(/index)).andExpect(MockMvcResultMatchers.redirectedUrl(index.html));比较返回内容使用content()// 返回内容为hellomockMvc.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具体如下RunWith(SpringRunner.class)SpringBootTestpublicclassUserServiceTest{AutowiredUserServiceuserService;Testpublicvoidtest(){Useruserthis.userService.findByName(scott);Assert.assertEquals(用户名为scott,scott,user.getUsername());}}运行后JUnit没有报错说明测试通过即UserService的findByName方法可行。此外和在Controller中引用Service相比在测试单元中对Service测试完毕后数据能自动回滚只需要在测试方法上加上Transactional注解比如:TestTransactionalpublicvoidtest(){UserusernewUser();user.setId(this.userService.getSequence(seq_user));user.setUsername(JUnit);user.setPasswd(123456);user.setStatus(1);user.setCreateTime(newDate());this.userService.save(user);}运行测试通过查看数据库发现数据并没有被插入这样很好的避免了不必要的数据污染。五、测试Controller现有如下ControllerRestControllerpublicclassUserController{AutowiredUserServiceuserService;GetMapping(user/{userName})publicUsergetUserByName(PathVariable(valueuserName)StringuserName){returnthis.userService.findByName(userName);}PostMapping(user/save)publicvoidsaveUser(RequestBodyUseruser){this.userService.saveUser(user);}}现在编写一个针对于该ControllergetUserByName(PathVariable(value “userName”) String userName)方法的测试类RunWith(SpringRunner.class)SpringBootTestpublicclassUserControllerTest{privateMockMvcmockMvc;AutowiredprivateWebApplicationContextwac;BeforepublicvoidsetupMockMvc(){mockMvcMockMvcBuilders.webAppContextSetup(wac).build();}Testpublicvoidtest()throwsException{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通过控制台输出过程如下所示MockHttpServletRequest: HTTP Method GET Request URI /user/scott Parameters {} Headers {Content-Type[application/json;charsetUTF-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;charsetUTF-8]} Content type application/json;charsetUTF-8 Body {id:23,username:scott,passwd:ac3af72d9f95161a502fd326865c2f15,createTime:1514535399000,status:1} Forwarded URL null Redirected URL null Cookies []继续编写一个针对于该ControllersaveUser(RequestBody User user)方法的测试类RunWith(SpringRunner.class)SpringBootTestpublicclassUserControllerTest{privateMockMvcmockMvc;AutowiredprivateWebApplicationContextwac;AutowiredObjectMappermapper;BeforepublicvoidsetupMockMvc(){mockMvcMockMvcBuilders.webAppContextSetup(wac).build();}TestTransactionalpublicvoidtest()throwsException{UserusernewUser();user.setUsername(Dopa);user.setPasswd(ac3af72d9f95161a502fd326865c2f15);user.setStatus(1);StringuserJsonmapper.writeValueAsString(user);mockMvc.perform(MockMvcRequestBuilders.post(/user/save).contentType(MediaType.APPLICATION_JSON_UTF8).content(userJson.getBytes())).andExpect(MockMvcResultMatchers.status().isOk()).andDo(MockMvcResultHandlers.print());}}运行过程如下所示MockHttpServletRequest: HTTP Method POST Request URI /user/save Parameters {} Headers {Content-Type[application/json;charsetUTF-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 []值得注意的是在一个完整的系统中编写测试单元时可能需要模拟一个登录用户信息SessionMockMvc也提供了解决方案可在初始化的时候模拟一个HttpSessionprivateMockMvcmockMvc;privateMockHttpSessionsession;AutowiredprivateWebApplicationContextwac;BeforepublicvoidsetupMockMvc(){mockMvcMockMvcBuilders.webAppContextSetup(wac).build();sessionnewMockHttpSession();UserusernewUser();user.setUsername(Dopa);user.setPasswd(ac3af72d9f95161a502fd326865c2f15);session.setAttribute(user,user);}