• SpringBoot测试及web环境模拟测试


    一、加载测试专用属性

    在很多测试下需要模拟一些线上情况,或者模拟一些特殊情况。但是在测试过程中,我们能不能每次测试的时候都去修改源码application.yml中的配置进行测试呢?显然是不行的。每次测试前改过来,每次测试后改回去,这太麻烦了。于是我们就想,需要在测试环境中创建一组临时属性,去覆盖我们源码中设定的属性,这样测试用例就相当于是一个独立的环境,能够独立测试,这样就方便多了。

    1. 临时属性

    SpringBoot已经为我们开发者早就想好了这种问题该如何解决,并且提供了对应的功能入口。在测试用例程序中,可以通过对注解@SpringBootTest添加属性来模拟临时属性,具体如下:
    首先我们先看一下我们的application.yml文件

    my:
      prop: hello-1
    
    • 1
    • 2

    未使用临时属性的测试代码

    @SpringBootTest
    class SpringBootTest2 {
    
        @Value("${my.prop}")
        String msg;
    
        @Test
        void contextLoads() {
            System.out.println(msg);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    使用临时属性的测试代码

    @SpringBootTest(properties = {"my.prop=hello-2"})
    class SpringBootTest2 {
    
        @Value("${my.prop}")
        String msg;
    
        @Test
        void contextLoads() {
            System.out.println(msg);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    通过临时属性的设置就能覆盖我们在yml中的属性设置了

    在这里插入图片描述

    2. 临时参数

    @SpringBootTest(args = {"--my.prop=hello-3"})
    class SpringBootTest2 {
    
        @Value("${my.prop}")
        String msg;
    
        @Test
        void contextLoads() {
            System.out.println(msg);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    临时参数就是使用 args 代替 临时属性 properties,同时他在每一个参数前都加上了--

    如果yml、临时属性、临时参数三种配置情况都存在的话SpringBoot会选择哪一种呢,我们来尝试一下。

    @SpringBootTest(args = {"--my.prop=hello-3"}, properties = {"my.prop=hello-2"})
    class SpringBootTest2 {
    
        @Value("${my.prop}")
        String msg;
    
        @Test
        void contextLoads() {
            System.out.println(msg);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    答案就是依据临时参数


    二、加载测试专用配置

    一个Spring环境中可以设置若干个配置文件或配置类,若干个配置信息可以同时生效。现在我们的需求就是在测试环境中再添加一个配置类,然后启动测试环境时,生效此配置就行了。其实做法和Spring环境中加载多个配置信息的方式完全一样。具体操作步骤如下:

    • 在测试包test中创建专用的测试环境配置类
      在这里插入图片描述
    @Configuration
    public class DBConfig {
        @Bean
        public String msg(){
            return "hello-db";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 在启动测试环境时,导入测试环境专用的配置类,使用@Import注解即可实现
    @SpringBootTest
    @Import(DBConfig.class)
    class SpringBootTest2 {
    
        @Autowired
        private String msg;
    
        @Test
        void contextLoads() {
            System.out.println(msg);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    定义测试环境专用的配置类,然后通过@Import注解在具体的测试中导入临时的配置,例如测试用例,方便测试过程,且上述配置不影响其他的测试类环境。


    三、Web环境模拟测试

    上述我们都是在数据层与业务层进行测试,那我们可以在表现层上进行测试吗?答案当然是可以。

    在对表现层功能进行测试需要一个基础和一个功能。所谓的一个基础是运行测试程序时,必须启动web环境,不然没法测试web功能。一个功能是必须在测试程序中具备发送web请求的能力,不然无法实现web功能的测试。所以在测试用例中测试表现层接口这项工作就转换成了两件事:

    • 如何在测试类中启动web测试
    • 如何在测试类中发送web请求

    1. 启动web测试

    每一个SpringBoot的测试类上方都会标准@SpringBootTest注解,而注解带有一个属性,叫做webEnvironment。通过该属性就可以设置在测试用例中启动web环境,具体如下:

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    class SpringBootWebTest {
    
    }
    
    • 1
    • 2
    • 3
    • 4

    webEnvironment属性值解释

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

    2. 测试类中发送请求

    • 定义好一个 controller
    @RestController
    @RequestMapping("/books")
    public class BookController {
    
    	@GetMapping
    	public String getBook(){
    		System.out.println("getBook" );
    		return "hello book";
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 模拟web请求调用过程
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    // 开启虚拟 MVC 调用
    @AutoConfigureMockMvc
    class SpringBootWebTest {
    
        // 定义发起虚拟调用的对象MockMVC,通过自动装配的形式初始化对象
        @Autowired
        private MockMvc mockMvc;
    
        @Test
        void testWeb() throws Exception {
            // 创建一个虚拟请求对象,封装请求的路径,并使用MockMVC对象发送对应请求
            RequestBuilder builder = MockMvcRequestBuilders.get("/book");
            mockMvc.perform(builder);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    可以看出,已经成功调用了 BookController。

    在这里插入图片描述

    3. 响应状态匹配

    在上述我们已经成功发出了请求,但是我们却无法得知表现层的功能是否正常,这就需要我们来进行一个判断。

    我们可以把这个过程理解为断言,当程序结果与我们预期相符时,测试通过;当不符时,抛出异常。

    在这里,我们可以通过判断响应状态,先来简单判断下 status。

    @Test
    void testWeb() throws Exception {
    	// 创建一个虚拟请求对象,封装请求的路径,并使用MockMVC对象发送对应请求
    	RequestBuilder builder = MockMvcRequestBuilders.get("/book");
    	ResultActions action = mockMvc.perform(builder);
    	// 设置预期值,与真实值比较. 成功测试通过; 失败测试不通过
    	// 定义本次调用成功的状态 200
    	StatusResultMatchers status = MockMvcResultMatchers.status();
    	// 预计本次调用时成功的 状态 200
    	ResultMatcher ok = status.isOk();
    	// 添加预计本次调用过程中进行匹配
    	action.andExpect(ok);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    如此后,控制台输出的仍然和上面一样,那怎么去看状态呢。

    在这里插入图片描述

    我们先来人为制造一个错误,设立一个不存在的路径。

    // 创建一个虚拟请求对象,发送到一个不存在的路径,制造错误
    RequestBuilder builder = MockMvcRequestBuilders.get("/book11111");
    
    • 1
    • 2

    如下图,当请求发生错误后(即真实结果与我们的预期值不匹配),控制台输出了错误原因。也得到了很多本次请求响应的结果。

    在这里插入图片描述

    通过以上我们也能得出一个结论,当真实值与我们的预期结果不匹配时,才会弹出错误。

    4. 响应体匹配

    • 定义一个 controller
    @RestController
    @RequestMapping("/book")
    public class BookController {
    
        @GetMapping("/one")
        public Book getBook2(){
            System.out.println("book get one");
            Book book = new Book();
            book.setId(1);
            book.setName("西游记");
            book.setPrice(20.0);
            return book;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 发送请求测试
    @Test
    void testBody(@Autowired MockMvc mockMvc) throws Exception {
    	RequestBuilder builder = MockMvcRequestBuilders.get("/book/one");
    	
    	ResultActions action = mockMvc.perform(builder);
    	// 设置预期值,与真实值比较,成功测试通过,失测试失败
    	// 定义本次调用成功的状态 200
    	ContentResultMatchers content = MockMvcResultMatchers.content();
    	// 预计本次调用时成功的 状态 200
    	ResultMatcher resultMatcher = content.json("{\"name\":\"西游记\",\"id\":1,\"price\":20.0}");
    	// 添加预计本次调用过程中进行匹配
    	action.andExpect(resultMatcher);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    5. 响应头匹配

     @Test
    public void testHeader(@Autowired MockMvc mvc) throws Exception {
    // 创建虚拟请求,当前访问/books
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/book");
    // 执行请求
    ResultActions action = mvc.perform(builder);
    // 匹配执行状态(是否预期值)
    // 定义执行状态匹配器
    HeaderResultMatchers header = MockMvcResultMatchers.header();
    ResultMatcher string = header.string("Content-Type", "application/json");
    // 使用本次真实值与预期结果对比
    action.andExpect(string);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    6. 完整信息匹配

    上述提到的各种响应匹配都可以放在一起来为请求做断言,只要有一个不匹配,程序都会抛出异常。

    @Test
    public void testGetById(@Autowired MockMvc mvc) throws Exception {
    	MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/book");
    	ResultActions action = mvc.perform(builder);
    	
    	StatusResultMatchers status = MockMvcResultMatchers.status();
    	ResultMatcher ok = status.isOk();
    	action.andExpect(ok);
    	
    	HeaderResultMatchers header = MockMvcResultMatchers.header();
    	ResultMatcher string = header.string("Content-Type", "application/json");
    	action.andExpect(string);
    	
    	ContentResultMatchers content = MockMvcResultMatchers.content();
    	ResultMatcher result = content.json("{\"name\":\"西游记\",\"id\":1,\"price\":20.0}");
    	action.andExpect(result);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    四、数据层测试数据回滚

    测试用例如果测试时产生了事务提交就会在测试过程中对数据库数据产生影响,进而产生垃圾数据。这个过程不是我们希望发生的,作为开发者测试用例该运行运行,但是过程中产生的数据不要在我的系统中留痕,这样该如何处理呢?

    SpringBoot早就为开发者想到了这个问题,并且针对此问题给出了最简解决方案,在原始测试用例中添加注解@Transactional即可实现当前测试用例的事务不提交。

    程序运行后,若注解@SpringBootTest出现的位置存在注解@Transactional,SpringBoot就会认为这是一个测试程序,无需提交事务,所以也就可以避免事务的提交。

    @SpringBootTest
    @Transactional
    @Rollback(true)
    public class DaoTest {
        @Autowired
        private BookService bookService;
    
        @Test
        void testSave(){
            Book book = new Book();
            book.setName("红楼梦");
            book.setId(2);
            book.setPrice(20.0);
            bookService.save(book);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 在SpringBoot的测试类中通过添加注解@Transactional来阻止测试用例提交事务
    • 通过注解@Rollback控制SpringBoot测试类执行结果是否提交事务,当为 true 时,回滚事务;false,不回滚事务。需要配合注解@Transactional使用

    五、测试用例数据设定

    对于测试用例的数据固定书写肯定是不合理的,SpringBoot提供了在配置中使用随机值的机制,确保每次运行程序加载的数据都是随机的。具体如下:

    testCase:
      book:
        id: ${random.int}
        name: ${random.value}
        price: ${random.int}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    @Component
    @Data
    @ConfigurationProperties(prefix = "testcase.book")
    public class BookCase {
        private int id;
        private String name;
        private Double price;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    @SpringBootTest
    @Transactional
    @Rollback(true)
    public class DaoTest {
    	@Autowired
    	private BookService bookService;
    	@Autowired
    	private BookCase bookCase;
    	
    	@Test
    	void testSave(){
    		Book book = new Book();
    		book.setName(bookCase.getName());
    		book.setId(bookCase.getId());
    		book.setPrice(bookCase.getPrice());
    		bookService.save(book);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    ${random.int}              # 随机整数
    ${random.int(10)}          # 10以内随机数
    ${random.int(10, 20)}    # 10 到 20 随机数
    ${random.uuid}             # 随机 uuid
    ${random.value}            # 随机字符串,MD5字符串,32位   
    ${random.long}             # 随机整数(long 范围)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 相关阅读:
    stack-es-标准篇-ElasticsearchClient-match_phrase_prefix
    CVE-2022-22947 SpringCloud GateWay SpEL RCE
    Bootstrap Blazor 使用模板创建项目
    深度学习(PyTorch)——循环神经网络(RNN)基础篇二
    Gradle引用本地Jar包
    高薪程序员&面试题精讲系列145之前后端如何交互?Swagger你用过吗?
    从零开始:如何通过美颜SDK构建自己的直播美颜工具
    (附源码)计算机毕业设计JavaJava毕设项目租车网站
    postman如何设置才能SwitchHosts切换host无缓存请求到指定ip服务
    el-date-picker限制开始时间不能大于结束时间,且结束不能小于开始时间
  • 原文地址:https://blog.csdn.net/qq_51938362/article/details/127660103