> 文档中心 > springboot单元测试

springboot单元测试


1.引入依赖

编写单元测试可以帮助开发人员编写高质量的代码,提升代码质量,减少Bug,便于重构。
Spring Boot提供了一些实用程序和注解,用来帮助我们测试应用程序,在Spring Boot中开启单元测试只需引入spring-boot-starter-test即可,其包含了一些主流的测试库。

引入spring-boot-starter-test:

    org.springframework.boot    spring-boot-starter-test    test

运行Maven命令dependency:tree可看到其包含了以下依赖:

[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:1.5.9.RELEASE:test[INFO] |  +- org.springframework.boot:spring-boot-test:jar:1.5.9.RELEASE:test[INFO] |  +- org.springframework.boot:spring-boot-test-autoconfigure:jar:1.5.9.RELEASE:test[INFO] |  +- com.jayway.jsonpath:json-path:jar:2.2.0:test[INFO] |  |  +- net.minidev:json-smart:jar:2.2.1:test[INFO] |  |  |  \- net.minidev:accessors-smart:jar:1.1:test[INFO] |  |  |     \- org.ow2.asm:asm:jar:5.0.3:test[INFO] |  |  \- org.slf4j:slf4j-api:jar:1.7.25:compile[INFO] |  +- junit:junit:jar:4.12:test[INFO] |  +- org.assertj:assertj-core:jar:2.6.0:test[INFO] |  +- org.mockito:mockito-core:jar:1.10.19:test[INFO] |  |  \- org.objenesis:objenesis:jar:2.1:test[INFO] |  +- org.hamcrest:hamcrest-core:jar:1.3:test[INFO] |  +- org.hamcrest:hamcrest-library:jar:1.3:test[INFO] |  +- org.skyscreamer:jsonassert:jar:1.4.0:test[INFO] |  |  \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test[INFO] |  +- org.springframework:spring-core:jar:4.3.13.RELEASE:compile[INFO] |  \- org.springframework:spring-test:jar:4.3.13.RELEASE:test
  • JUnit,标准的单元测试Java应用程序;
  • Spring Test & Spring Boot Test,对Spring Boot应用程序的单元测试提供支持;
  • Mockito, Java mocking框架,用于模拟任何Spring管理的Bean,比如在单元测试中模拟一个第三方系统Service接口返回的数据,而不会去真正调用第三方系统;
  • AssertJ,一个流畅的assertion库,同时也提供了更多的期望值与测试返回值的比较方式;
  • Hamcrest,库的匹配对象(也称为约束或谓词);
  • JsonPath,提供类似XPath那样的符号来获取JSON数据片段;
  • JSONassert,对JSON对象或者JSON字符串断言的库。

一个标准的Spring Boot测试单元应有如下的代码结构:

@DisplayName("AlarmMsgstationController测试类")@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)@AutoConfigureMockMvc@TestInstance(TestInstance.Lifecycle.PER_CLASS)@Transactional@TestMethodOrder(MethodOrderer.OrderAnnotation.class)public class AlarmMsgstationControllerTest {}

2.JUnit4注解

JUnit4中包含了几个比较重要的注解:@BeforeClass、@AfterClass、@Before、@After和@Test。其中, @BeforeClass和@AfterClass在每个类加载的开始和结束时运行,必须为静态方法;而@Before和@After则在每个测试方法开始之前和结束之后运行。见如下例子:

@RunWith(SpringRunner.class)@SpringBootTestpublic 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");    }}
输出的运行顺序...before class testbefore testtest 1+1=2after testbefore testtest 2+2=4after testafter class test

3、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数组是否相等。

4、MockMvc

MockMvc是服务端 Spring MVC测试支持的主入口点。

可以用来模拟客户端请求,用于测试MockMvc,从字面上来看指的是模拟的MVC,即其可以模拟一个MVC环境,向Controller发送请求然后得到响应。

在单元测试中,使用MockMvc前需要进行初始化,如下所示:

@Autowiredprivate MockMvc mockMvc;@Autowiredprivate WebApplicationContext wac;@Beforepublic 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")));

模拟请求参数:

// 模拟发送一个message参数,值为hellomockMvc.perform(MockMvcRequestBuilders.get("/hello").param("message", "hello"));// 模拟提交一个checkbox值,name为hobby,值为sleep和eatmockMvc.perform(MockMvcRequestBuilders.get("/saveHobby").param("hobby", "sleep", "eat"));

也可以直接使用MultiValueMap构建参数:

MultiValueMap params = new LinkedMultiValueMap();params.add("name", "mrbird");params.add("hobby", "sleep");params.add("hobby", "eat");mockMvc.perform(MockMvcRequestBuilders.get("/hobby/save").params(params));

模拟发送JSON参数:

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技术),如下所示:

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:

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));

完整示例

@DisplayName("AlarmMsgstationController测试类")@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)@AutoConfigureMockMvc@TestInstance(TestInstance.Lifecycle.PER_CLASS)@Transactional@TestMethodOrder(MethodOrderer.OrderAnnotation.class)public class AlarmMsgstationControllerTest {    @Autowired    private MockMvc mockMvc;    @Autowired    private ShiroController shiroController;    private String token;    private ListUserVO testUser;    @BeforeAll    void setUpAll() { // 以系统管理员身份登录系统 LoginDTO loginDTO = new LoginDTO(); loginDTO.setUserName(""); loginDTO.setPassword(""); 。。。。。。。。。。。。。。。。。。。。。。。    }    @AfterAll    void tearDownAll() { shiroController.logout(token);    }    @BeforeEach    void setUp() { System.out.println("************************" + "函数测试开始" + "***************************");    }    @AfterEach    void tearDown() { System.out.println("************************" + "函数测试结束" + "***************************");    }    @Test    @DisplayName("获取告警方式列表测试")    @Order(1)    @Rollback(false)    void listAlarmMsgstationTest() throws Exception { // 模拟用户数据 MultiValueMap params = new LinkedMultiValueMap(); params.add("packageNum", Integer.toString(1)); params.add("packageSize", Integer.toString(10)); params.add("orderFields", "id"); params.add("order", "desc"); params.add("searchFields", "username"); params.add("search", "hhhh"); // 模拟请求 String url = "/alarm/alarmMsgstations"; RequestBuilder req = MockMvcRequestBuilders.get(url).header("token", token).header("timed_task", false)  .contentType(MediaType.APPLICATION_JSON_UTF8).params(params); MvcResult result = mockMvc.perform(req).andReturn(); // 获取请求结果 result.getResponse().setCharacterEncoding("UTF-8"); int httpStatus = result.getResponse().getStatus(); String content = result.getResponse().getContentAsString(); JSONObject jsonObject = JSON.parseObject(content); Integer resultCode = (Integer) jsonObject.get("resultCode"); String message = (String) jsonObject.get("message"); JSONArray jsonUserAlarmMsgstations = jsonObject.getJSONObject("data").getJSONArray("content"); // 获取新增用户信息 List alarmMsgstationList = JSONObject.parseArray(jsonUserAlarmMsgstations.toJSONString(),  ListAlarmMsgstationVO.class); System.out.println("response content:" + content); // 请求结果断言测试 // 返回码 Assertions.assertTrue(httpStatus == HttpStatus.OK.value()); // 响应内容 Assertions.assertAll("response content test",  () -> Assertions.assertEquals(ResultCode.SUCCESS.getCode(), resultCode),  () -> Assertions.assertEquals("查询成功", message), () -> Assertions.assertNotNull(alarmMsgstationList)); Assertions.assertTrue(content.contains("\"packageNum\":1")); Assertions.assertTrue(content.contains("\"packageSize\":10"));    }