一、前言
各位小伙伴们,上次已经讲解了用SpringBoot实现技术整合,体验SpringBoot的简化开发,今天这一篇介绍代码的测试。测试是保障程序正确性的唯一屏障,在企业级开发中更是不可缺少,但是由于测试代码往往不产生实际效益,所以在发开中,有时会跳过测试,我们往往这一块比较短板,所以还是要拿出来把这一块知识好好说说,做一名专业的开发人员。
本篇文章使用springboot技术,实现测试功能!
二、测试专用属性
测试过程本身并不是一个复杂的过程,但是很多情况下测试时需要模拟一些线上情况,或者模拟一些特殊情况。如果当前环境按照线上环境已经设定好了,例如是下面的配置:
person: name: "tom" age: 18
但是你现在想测试对应的兼容性,需要测试如下配置:
person: name: "cat" age: 20
这个时候我们不能每次测试的时候都去修改源码application.yml中的配置进行测试呢?每次测试前改过来,每次测试后改回去,这太麻烦了。于是我们就想,需要在测试环境中创建一组临时属性,去覆盖我们源码中设定的属性,这样测试用例就相当于是一个独立的环境,能够独立测试,这样就方便多了。下面就来介绍该如何解决这个问题:
1、临时属性
假如我有一个application.yml文件,配置是这样的:
test: prop: testValue1
当我准备测试的时候,在不修改源码的情况下,想改变prop属性为testValue2,该怎么办呢?
在测试用例程序中,可以通过对注解@SpringBootTest添加属性来模拟临时属性,具体如下:
@Slf4j @SpringBootTest(properties = {"test.prop=testValue2"}) class PropertiesAndArgsTest { @Value("${test.prop}") private String msg; @Test void test01() { log.info(msg); } }
看一看运行结果:
使用注解@SpringBootTest的properties属性就可以为当前测试用例添加临时的属性,覆盖源码配置文件中对应的属性值进行测试。
2、临时参数
除了上述这种情况,在使用命令行启动springboot程序时,通过命令行参数也可以设置属性值。而且线上启动程序时,通常都会添加一些专用的配置信息。作为开发者提供了对应的书写内容后,能否提前测试一下这些配置信息是否有效呢?当时是可以的,还是通过注解@SpringBootTest的另一个属性来进行设定。
@Slf4j @SpringBootTest(properties = {"test.prop=testValue2"},args = {"--test.prop=testValue3"}) class PropertiesAndArgsTest { @Value("${test.prop}") private String msg; @Test void test01() { log.info(msg); } }
运行结果:
使用注解@SpringBootTest的args属性就可以为当前测试用例模拟命令行参数并进行测试。
说到这里,好奇的小伙伴们可能会问,如果两者共存呢?其实如果思考一下配置属性与命令行参数的加载优先级,这个结果就不言而喻了。在属性加载的优先级设定中,有明确的优先级设定顺序,让我们打开官网:SpringBoot官网文档
在这个属性加载优先级的顺序中,明确规定了命令行参数的优先级排序是11,而配置性的优先级是3,结果不言而喻了,args属性配置优先于properties属性配置加载。
总结:
加载测试临时属性可以通过注解@SpringBootTest的properties和args属性进行设定,此设定应用范围仅适用于当前测试用例
三、测试专用配置
如果想在测试的时候临时加载一些Bean能不做呢?也就是说我们测试时,想搞一些独立的Bean出来,专门应用于测试环境,能否实现呢?答案是当然可以,具体操作步骤如下:
步骤①:在测试包test中创建专用的测试环境配置类,是在test包中!!
@Configuration public class MsgConfig { @Bean public String msg() { return "Bean Message"; } }
上述配置仅用于演示当前实验效果,实际开发可不能这么注入String类型的数据
步骤②:在启动测试环境时,导入测试环境专用的配置类
@Slf4j @SpringBootTest public class ConfigurationTest { @Autowired private String msg; @Test void test01() { log.info(msg); } }
运行结果:
这样我们就可以实现每一个不同的测试用例加载不同的bean的效果,丰富测试用例的编写,同时不影响开发环境的配置。
四、Web环境模拟测试
实际企业开发不仅要保障业务层与数据层的功能安全有效,也要保障表现层的功能正常。但是我们目的对表现层的测试都是通过postman手工测试的,并没有在打包过程中体现表现层功能被测试通过。能否在测试用例中对表现层进行功能测试呢?答案也是可以的!
在测试中对表现层功能进行测试需要一个基础和一个功能。所谓的一个基础是运行测试程序时,必须启动web环境,不然没法测试web功能。一个功能是必须在测试程序中具备发送web请求的能力,不然无法实现web功能的测试。所以在测试用例中测试表现层接口这项工作就转换成了两件事,如何在测试类中启动web测试?如何在测试类中发送web请求?
1、测试类中启动web环境
每一个springboot的测试类上方都会标准@SpringBootTest注解,而注解带有一个属性,叫做webEnvironment。通过该属性就可以设置在测试用例中启动web环境,具体如下:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WebTest { }
测试类中启动web环境时,可以指定启动的Web环境对应的端口,springboot提供了4种设置值,分别如下:
- MOCK:根据当前设置确认是否启动web环境,例如使用了Servlet的API就启动web环境,属于适配性的配置
- DEFINED_PORT:使用自定义的端口作为web服务器端口
- RANDOM_PORT:使用随机端口作为web服务器端口
- NONE:不启动web环境
建议大家测试时使用RANDOM_PORT,避免代码中因为写死设定引发线上功能打包测试时由于端口冲突导致意外现象的出现。就是说你程序中写了用8080端口,结果线上环境8080端口被占用了,结果你代码中所有写的东西都要改,这就是写死代码的代价。现在你用随机端口就可以测试出来你有没有这种问题的隐患了。
测试环境中的web环境已经搭建好了,下面就可以来解决第二个问题了,如何在程序代码中发送web请求。
2、测试类中发送请求
先提前写一个controller,用于后面的测试,代码如图:
我们该在测试类中如何发送请求呢?具体操作如下:
步骤①:在测试类中开启web虚拟调用功能,通过注解@AutoConfigureMockMvc实现此功能的开启
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //日志调用 @Slf4j //开启虚拟MVC调用 @AutoConfigureMockMvc public class WebTest { }
步骤②:定义发起虚拟调用的对象MockMVC,通过自动装配的形式初始化对象
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Slf4j @AutoConfigureMockMvc public class WebTest { @Test void test01(@Autowired MockMvc mvc) { } }
步骤③:创建一个虚拟请求对象,封装请求的路径,并使用MockMVC对象发送对应请求
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Slf4j @AutoConfigureMockMvc public class WebTest { @Test void test01(@Autowired MockMvc mvc) throws Exception { //创建虚拟请求,当前访问/books MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/books"); //执行对应的请求 mvc.perform(requestBuilder); } }
执行测试程序,现在就可以正常的发送/books对应的请求了,注意访问路径不要写
http://localhost:8080/books,因为前面的服务器IP地址和端口使用的是当前虚拟的web环境,无需指定,仅指定请求的具体路径即可。
总结:
- 在测试类中测试web层接口要保障测试类启动时启动web容器,使用@SpringBootTest注解的webEnvironment属性可以虚拟web环境用于测试
- 为测试方法注入MockMvc对象,通过MockMvc对象可以发送虚拟请求,模拟web请求调用过程
3、web环境请求结果比对
目前已经成功的发送了请求,但是还没有起到测试的效果,测试过程必须出现预计值与真实值的比对结果才能确认测试结果是否通过,虚拟请求中能对哪些请求结果进行比对呢?其实发完请求得到的信息只有一种,就是响应对象。至于响应对象中包含什么,就可以比对什么。常见的比对内容如下:
- 响应状态匹配
@Test void test02(@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/books"); ResultActions actions = mvc.perform(requestBuilder); //设定预期值,与真实值进行比较,成功测试通过,失败测试失败 //定义本次调用的预期值 StatusResultMatchers status = MockMvcResultMatchers.status(); //预计本次调用时成功:状态200 ResultMatcher ok = status.isOk(); //添加预计值到本次调用过程中进行匹配 actions.andExpect(ok); }
正确结果:
假如我们把地址改为/bookss,就会报错:
响应体匹配(非json数据格式)
@Test void test03(@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/books"); ResultActions actions = mvc.perform(requestBuilder); ContentResultMatchers content = MockMvcResultMatchers.content(); ResultMatcher result = content.string("Hello~~"); actions.andExpect(result); }
我们再写一个controller用于测试新的内容,代码如下图:
响应体匹配(json数据格式,开发中的主流使用方式)
@Test void test04(@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/books/getJson"); ResultActions actions = mvc.perform(requestBuilder); ContentResultMatchers content = MockMvcResultMatchers.content(); ResultMatcher result = content.json("{\"name\":\"cabbage\",\"age\":21,\"email\":\"cabbage@qq.com\"}"); actions.andExpect(result); } }
响应头信息匹配
@Test void test05(@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/books/getJson"); ResultActions actions = mvc.perform(requestBuilder); HeaderResultMatchers header = MockMvcResultMatchers.header(); ResultMatcher contentType = header.string("Content-Type", "application/json"); actions.andExpect(contentType); } }
基本上齐了,头信息,正文信息,状态信息都有了,就可以组合出一个完美的响应结果比对结果了。以下范例就是三种信息同时进行匹配校验,也是一个完整的信息匹配过程。
@Test void testGetById(@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/getJson"); ResultActions action = mvc.perform(builder); StatusResultMatchers status = MockMvcResultMatchers.status(); ResultMatcher ok = status.isOk(); action.andExpect(ok); HeaderResultMatchers header = MockMvcResultMatchers.header(); ResultMatcher contentType = header.string("Content-Type", "application/json"); action.andExpect(contentType); ContentResultMatchers content = MockMvcResultMatchers.content(); ResultMatcher result = content.json("{\"name\":\"cabbage\",\"age\":21,\"email\":\"cabbage@qq.com\"}"); action.andExpect(result); }
总结:
web虚拟调用可以对本地虚拟请求的返回响应信息进行比对,分为响应头信息比对、响应体信息比对、响应状态信息比对
五、数据层测试回滚
测试用例如果测试时产生了事务提交就会在测试过程中对数据库数据产生影响,进而产生垃圾数据。这个过程不是我们希望发生的,作为开发者测试用例该运行运行,但是过程中产生的数据不要在我的系统中留痕,这样该如何处理呢?
我们只需要在原始测试用例中添加注解@Transactional即可实现当前测试用例的事务不提交。当程序运行后,只要注解@Transactional出现的位置存在注解@SpringBootTest,springboot就会认为这是一个测试程序,无需提交事务,所以也就可以避免事务的提交。
我们先创建一个新的表,表里面没有任何数据,如图:
创建对应的实体类,配置好数据库连接的环境,这些就不仔细介绍了,不会的小伙伴们,可以去我的专栏中查找~
开始测试:
@SpringBootTest @Transactional @Rollback(false) public class SqlTest { @Autowired private UserMapper userMapper; @Test void test01() { User user = new User(); user.setName("cabbage"); user.setAge(20); user.setEmail("cabbage@qq.com"); userMapper.insert(user); } }
上述代码运行后并不会对数据库产生影响,但是如果把false改为true,再次执行代码,数据库就会出现垃圾数据,如图所示:
如果我们想提交事物,只需要添加一个@RollBack的注解,设置回滚状态为false即可正常提交事务,是不是很方便呢?springboot在辅助开发者日常工作这一块展现出了惊人的能力,实在太贴心了。
总结:
- 在springboot的测试类中通过添加注解@Transactional来阻止测试用例提交事务
- 通过注解@Rollback控制springboot测试类执行结果是否提交事务,需要配合注解@Transactional使用
六、测试用例数据设定
对于测试用例的数据固定书写肯定是不合理的,springboot提供了在配置中使用随机值的机制,确保每次运行程序加载的数据都是随机的。具体如下:
testcase: book: id: ${random.int} id2: ${random.int(10)} type: ${random.int!5,10!} name: ${random.value} uuid: ${random.uuid} publishTime: ${random.long}
当前配置就可以在每次运行程序时创建一组随机数据,避免每次运行时数据都是固定值的尴尬现象发生,有助于测试功能的进行。数据的加载按照之前加载数据的形式,使用@ConfigurationProperties注解即可
@Component @Data @ConfigurationProperties(prefix = "testcase.book") public class BookCase { private int id; private int id2; private int type; private String name; private String uuid; private long publishTime; }
让我们写个代码测试一下:
@SpringBootTest public class RandomTest { @Autowired private BookCase bookCase; @Test void test01() { System.out.println(bookCase.getId()); System.out.println(bookCase.getName()); } }
运行结果:
对于随机值的产生,还有一些小的限定规则,比如产生的数值性数据可以设置范围等,具体如下:
- ${random.int}表示随机整数
- ${random.int(10)}表示10以内的随机数
- ${random.int(10,20)}表示10到20的随机数
- 其中()可以是任意字符,例如[],!!均可
七、总结
这一篇总结了常用的代码测试的方法,去大厂真的是必不可少!希望小伙伴们都可以认真掌握,创作不易,喜欢的话,可以给博主三连支持哦~~