springboot:测试——controller 层测试,service 层测试

1,373 阅读1分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第14天,点击查看活动详情

测试

加载测试专用属性

  • 我们通过@SpringBootTest(properties = {"属性名 = 属性值","属性名 = 属性值"}来加载测试专用属性

    【配置文件】

    test:
      prop: "hyz"
    

    【测试】

    @SpringBootTest(properties = {"test.prop=hgd", "test.ttt=ttt"})
    public class PropertiesAndArgsTest {
    
        @Value("${test.prop}")
        String name;
    
        @Value("${test.ttt}")
        String ttt;
    
        @Test
        void test1() {
            System.out.println(name);
            System.out.println(ttt);
        }
    }
    

    【结果】

    image-20220428172859723

    **测试专用属性覆盖了配置文件设置的属性。**以后如果像做特殊测试的时候可以不修改配置文件直接通过注解的方式来完成。

  • 通过@SpringBootTest(args = {"--属性名 = 属性值, "--属性名 = 属性值"})来加载测试专用属性

    【配置文件】

    test:
      prop: "hyz"
    

    【测试】

    @SpringBootTest(args = {"--test.prop=hgd", "--test.ttt=ttt"})
    public class PropertiesAndArgsTest {
    
        @Value("${test.prop}")
        String name;
    
        @Value("${test.ttt}")
        String ttt;
    
        @Test
        void test1() {
            System.out.println(name);
            System.out.println(ttt);
        }
    }
    

    【结果】

    image-20220428173402478

    注解的argsproperties 的结果是一致的,只不过args在属性名之前需要添加 --而已。

  • 接下来测试配置文件赋值,properties属性赋值和args属性赋值

    【配置文件】

    test:
      prop: "resource"
    

    【测试】

    @SpringBootTest(properties = {"test.prop=properties"}, args = {"--test.prop=args"})
    public class PropertiesAndArgsTest {
    
        @Value("${test.prop}")
        String name;
    
        @Test
        void test1() {
            System.out.println(name);
        }
    }
    

    【结果】

    image-20220428173848227

    可以看到最终显示的是 args的属性值。我们根据之前的结论可以得出:args>properties> 配置文件

【总结】

  • args@SpringBootTest(args = {"--属性名 = 属性值, "--属性名 = 属性值"})来加载测试专用属性
  • properties@SpringBootTest(properties = {"属性名 = 属性值","属性名 = 属性值"}来加载测试专用属性
  • 属性覆盖顺序:args>properties> 配置文件

加载测试专用配置

我们在做单元测试的时候,但又不想放入源码级别的代码当中,所以我们选择方法测试下写一个测试所需配置的配置类。

【创建测试配置类】

@Configuration
public class MsgConfig {
    
    @Bean
    public String msg() {
        return "bean msg";
    }
}

【测试类】

我们的测试类是自动导入源码级别代码的所有配置类,但是我们创建的测试配置类并没有导入,所以我们需要先使用@Import({配置类.class},{配置类.class})来手动给导入我们的配置类。

导入完成之后用 @Autowired来自动装载,就可以进行测试了。

@SpringBootTest
@Import({MsgConfig.class})
public class ConfigurationTest {

    @Autowired
    private String msg;

    @Test
    public void test() {
        System.out.println(msg);
    }
}

【结果】

我们成功将配置类的信息输出。而在其他测试类中并不会引入这个配置类,这样子我们就达到了局部导入配置类的目的。

image-20220428183715407

【总结】

  • 使用 @Import({配置类.class},{配置类.class})注解加载测试类专用配置。
  • 可以将一些测试用例封装成类,再导入就可以达到局部导入配置类的目的了。

测试 web 端

启动 web 环境

我们通过注解@SpringBootTest(webEnvironment = 类别)指明 environment ,一共有四个类别:

  • MOCK:此值为默认值,该类型提供一个mock环境,可以和@AutoConfigureMockMvc或@AutoConfigureWebTestClient搭配使用,开启Mock相关的功能。注意此时内嵌的服务(servlet容器)并没有真正启动,也不会监听web服务端口。
  • RANDOM_PORT:启动一个真实的web服务,监听一个随机端口。
  • DEFINED_PORT:启动一个真实的web服务,监听一个定义好的端口(从application.properties读取)。
  • NONE:启动一个非web的ApplicationContext,既不提供mock环境,也不提供真实的web服务。

image-20220428195526134

【测试】

  • RANDOM_PORT(随机端口)

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    public class WebTest {
        @Test
        void test() {
    
        }
    }
    

    【结果】

    image-20220428200447501

  • DEFINED_PORT(默认端口)

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
    public class WebTest {
        @Test
        void test() {
    
        }
    }
    

    【结果】

    image-20220428200559993

因为我尝试过使用配置文件设置端口号,但是会受到测试专用属性的影响

使用@SpringBootTest(args = {"--属性名 = 属性值, "--属性名 = 属性值"})

image-20220428201522142

使用@SpringBootTest(properties = {"属性名 = 属性值","属性名 = 属性值"}

image-20220428201631169

【总结】

  • 通过@SpringBootTest(webEnvironment = 类别)来选择 web 端口是否开启
  • RANDOM_PORT:随机端口
  • DEFINED_PORT:默认端口
  • DEFINED_PORT并不会受到配置文件的影响。
  • 端口会受到测试专用属性的影响

发送虚拟请求

我们测试中开启了 web 环境之后就要对 controller 层的代码进行测试。下面我们就来一起写一下

  1. 创建 controller 层的代码(如果是实际中测试,controller 层的代码已经存在)

    @RestController
    @RequestMapping("/books")
    public class BookController {
        @GetMapping
        public String getById() {
            System.out.println("*************** getById is running... ***************");
            return "springboot";
        }
    }
    
  2. 在测试类中开启 MVC 调用并启动 web 环境

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    // 开启虚拟 MVC 的调用
    @AutoConfigureMockMvc
    public class WebTest {
       
    }
    
  3. 注入虚拟 MVC 调用对象

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    // 开启虚拟 MVC 的调用
    @AutoConfigureMockMvc
    public class WebTest {
        @Test
        // 注入虚拟 MVC 调用对象
        void webTest(@Autowired MockMvc mvc) throws Exception {
            
        }
    }
    
  4. 模拟请求,访问当前端口 / books (就是 controller 层的被测试方法的端口)

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    // 开启虚拟 MVC 的调用
    @AutoConfigureMockMvc
    public class WebTest {
        @Test
            // 注入虚拟 MVC 调用对象
        void webTest(@Autowired MockMvc mvc) throws Exception {
            // 模拟请求,访问当前端口/books
            MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
           
        }
    }
    
  5. 使用虚拟 MVC 对象执行 get 请求。如果测试方法是其他请求格式,模拟请求也可以请求对应的请求格式。

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    // 开启虚拟 MVC 的调用
    @AutoConfigureMockMvc
    public class WebTest {
        @Test
            // 注入虚拟 MVC 调用对象
        void webTest(@Autowired MockMvc mvc) throws Exception {
            // 模拟请求,访问当前端口/books
            MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
            // 执行对应请求
            mvc.perform(builder);
        }
    }
    
  6. 测试结果

    虚拟 MVC 对象执行成功,控制台显示了测试方法被调用。

    image-20220428205924487


匹配响应执行状态

我们通过设定预期值和真实值相比,成功测试通过,失败测试失败。

首先我们要有一个 controller 类

@RestController
@RequestMapping("/books")
public class BookController {
    @GetMapping
    public String getById() {
        System.out.println("*************** getById is running... ***************");
        return "result test";
    }
}
  1. 创建代表预测响应头状态的对象

    @Test
    void StatusTest(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
     
        // 创建代表预测状态的对象
        StatusResultMatchers status = MockMvcResultMatchers.status();
    }
    
  2. 设定预测状态

    @Test
    void StatusTest(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
    
        // 创建代表预测状态的对象
        StatusResultMatchers status = MockMvcResultMatchers.status();
        // 预计本次调用的状态:成功,状态200
        ResultMatcher result = status.isOk();
    }
    
  3. 预测状态与本次实际状态进行匹配

    @Test
    void StatusTest(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
    
        // 设定预期值与真实值比较,成功测试通过,失败测试失败
        // status代表当前模拟运行的状态
        StatusResultMatchers status = MockMvcResultMatchers.status();
        // 预计本次调用的状态:成功,状态200
        ResultMatcher result = status.isOk();
        // 添加预计状态与本次实际状态进行匹配。
        action.andExpect(result);
    }
    
    • 成功

      image-20220429110916787

    • 失败

      我修改了模拟请求的访问端口,将 /books改成了/books1。这样子实际状态将不符合我们设置的预计状态。

      image-20220429111100828


匹配响应体(字符串)

通过比较字符串

创建 controller 类

@RestController
@RequestMapping("/books")
public class BookController {
    @GetMapping
    public String getById() {
        System.out.println("*************** getById is running... ***************");
        return "RestFul test";
    }
}
  1. 创建对请求返回的内容进行验证的对象

    @Test
    void BodyTest(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
    
        // 创建对请求返回的内容进行验证的对象
        ContentResultMatchers content = MockMvcResultMatchers.content();
    }
    
  2. 设定预定返回内容

    @Test
    void BodyTest(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
    
        // 创建对请求返回的内容进行验证的对象
        ContentResultMatchers content = MockMvcResultMatchers.content();
        // 设定预定返回内容
        ResultMatcher result = content.string("springboot");
    }
    
  3. 与实际返回内容进行比对

    @Test
    void BodyTest(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
    
        // 创建对请求返回的内容进行验证的对象
        ContentResultMatchers content = MockMvcResultMatchers.content();
        // 设定预定返回内容
        ResultMatcher result = content.string("springboot");
        // 与实际返回内容进行比对
        action.andExpect(result);
    }
    
    • 成功

      image-20220429112621681

    • 失败

      修改了预定返回内容,RestFul Test改为Test。这样子实际返回内容将不同于我们预设返回内容。

      image-20220429112801356


匹配响应体(Json)

在日常开发的过程中,我们不会直接返回字符串,而是会返回一个 Json 字符串。匹配 Json 的方法和匹配字符串大致相同,只有预定返回内容会有变化。

创建 controller 类

@RestController
@RequestMapping("/books")
public class BookController {
    @GetMapping
    public String getById() {
        System.out.println("*************** getById is running... ***************");
        return "RestFul test";
    }
}

创建前后端数据规范

@Data
public class R {
    private Boolean flag;
    private Object data;
    private String msg;

    public R() {
    }

    public R(Boolean flag) {
        this.flag = flag;
    }

    public R(Boolean flag, Object data) {
        this.flag = flag;
        this.data = data;
    }

    public R(Boolean flag, String msg) {
        this.flag = flag;
        this.msg = msg;
    }
}
  1. 创建对请求返回的内容进行验证的对象

    @Test
    void BodyTest(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
    
        // 创建对请求返回的内容进行验证的对象
        ContentResultMatchers content = MockMvcResultMatchers.content();
    }
    
  2. 设定预定返回内容,使用 public ResultMatcher json(String jsonContent)来指定 json 字符串。

    @Test
    void JsonTest(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
    
        // 创建对请求返回的内容进行验证的对象行的结果
        ContentResultMatchers content = MockMvcResultMatchers.content();
        // 设定预定返回内容
        ResultMatcher result = content.json("{\"flag\":true,\"data\":{\"bookID\":1,\"bookName\":\"java\",\"bookCounts\":10,\"detail\":\"java is the best coding language in the world\"},\"msg\":null}");
        // 添加预计状态与本次实际状态进行匹配。
        action.andExpect(result);
    }
    
  3. 与实际返回内容进行比对

    @Test
    void JsonTest(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
    
        // 创建对请求返回的内容进行验证的对象行的结果
        ContentResultMatchers content = MockMvcResultMatchers.content();
        // 设定预定返回内容
        ResultMatcher result = content.json("{\"flag\":true,\"data\":{\"bookID\":1,\"bookName\":\"java\",\"bookCounts\":10,\"detail\":\"java is the best coding language in the world\"},\"msg\":null}");
        // 与实际返回内容进行比对
        action.andExpect(result);
    }
    
    • 正确

      image-20220429120544998

    • 错误

      修改预定返回内容

      image-20220429120635565


匹配响应头

创建 controller 类

@RestController
@RequestMapping("/books")
public class BookController {
    @GetMapping
    public String getById() {
        System.out.println("*************** getById is running... ***************");
        return "RestFul test";
    }
}
  1. 创建匹配响应头的对象

    @Test
    void ContentTypeTest(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
    
        // 创建匹配响应头的对象
        HeaderResultMatchers header = MockMvcResultMatchers.header();
    }
    
  2. 设定预计响应头

    @Test
    void ContentTypeTest(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
    
        // 创建匹配响应头的对象
        HeaderResultMatchers header = MockMvcResultMatchers.header();
        // 设定预计响应头
        ResultMatcher contentType = header.string("Content-Type", "application/json");
    }
    
  3. 与实际响应头进行对比

    @Test
    void ContentTypeTest(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        ResultActions action = mvc.perform(builder);
    
        // 创建匹配响应头的对象
        HeaderResultMatchers header = MockMvcResultMatchers.header();
        // 设定预计响应头
        ResultMatcher contentType = header.string("Content-Type", "application/json");
        // 与实际响应头进行对比
        action.andExpect(contentType);
    }
    
    • 正确

      image-20220429121337986

    • 错误

      修改预计响应头

      image-20220429121434157


Service 层测试事务回滚

我们在对 Service 层进行测试的时候,常常会使用测试用例进行测试。但是这些测试用例测试完之后就会留在数据库里作为垃圾数据的存在。SpringBoot 这里就为我们提供了一个接口 @Transactional来避免这种情况的发生。

我们在 Service 层测试类里添加了 @Transactional接口之后,对 Service 层使用测试用例进行测试,最后发现全部的测试用例没有留在数据库里。数据库完成测试之后就会自动回滚。

  • 未添加注解前

    @SpringBootTest
    public class ControllerTest {
        @Autowired
        private BooksService booksService;
    
        @Test
        void test() {
            Books books = new Books(null, "测试用例1", 10, "测试用例1");
            booksService.save(books);
        }
    }
    

    image-20220429151415986

  • 添加注解后

    @SpringBootTest
    @Transactional
    public class ControllerTest {
        @Autowired
        private BooksService booksService;
    
        @Test
        void test() {
            Books books = new Books(null, "测试用例2", 10, "测试用例2");
            booksService.save(books);
        }
    }
    

    数据没有写入

    image-20220429151635568

    控制台也有提示

    image-20220429151824560

【总结】

  • 进行 Service 层测试的时候添加 @Transactional来将事务回滚。

测试用例设置随机数据

我们在测试的测试用例可以使用随机数据代替。

  1. 首先在配置文件里写下测试用例数据

    testcase:
      book:
        id: ${random.int} # 随机整数
        id2: ${random.int(10)} # 10以内整数
        id3: ${random.int(50,100)} # 50到100随机整数
        uuid: ${random.uuid} # 随机uuid
        name: ${random.value} # 随机字符串,MD5字符串,32位
        long-type: ${random.long} # 随机整数(Long范围)
    
  2. 用一个类来接收

    @Data
    @Component
    @ConfigurationProperties(value = "testcase.book")
    public class BookCase {
        private int id;
        private int id2;
        private int id3;
        private String uuid;
        private String name;
        private long longType;
    }
    
  3. 将数据输出

    @SpringBootTest
    public class PropertiesAndArgsTest {
    
        @Autowired
        private BookCase bookCase;
    
        @Test
        void test1() {
            System.out.println(bookCase.getId());
            System.out.println(bookCase.getId2());
            System.out.println(bookCase.getId3());
            System.out.println(bookCase.getUuid());
            System.out.println(bookCase.getName());
            System.out.println(bookCase.getLongType());
        }
    }
    
  4. 结果

    image-20220429162320785

【总结】

  • ${random.int}:随机整数
  • ${random.int ( max )}:max以内整数
  • ${random.int( min, max )}:min 到 max 随机整数
  • ${random.uuid}:随机uuid
  • ${random.value}:随机字符串,MD5字符串,32位
  • ${random.long}:随机整数(Long范围)
  • ${random.long ( max )}:max以内整数(Long范围)
  • ${random. long( min, max )}:min 到 max 随机整数(Long范围)
  • 我们在测试 Service 层的时候,可以将数据随机传入数据库