测试
测试是保障程序正确性的唯一屏障,在企业级开发中更是不可缺少,但是由于测试代码往往不产生实际效益,所以一些小型公司并不是很关注,往往这一块比较短板
一、加载测试专用属性
测试过程本身并不是一个复杂的过程,但是很多情况下测试时需要模拟一些线上情况,或者模拟一些特殊情况。如果当前环境按照线上环境已经设定好了,例如是下面的配置
env:
maxMemory: 32GB
minMemory: 16GB
但是你现在想测试对应的兼容性,需要测试如下配置
env:
maxMemory: 16GB
minMemory: 8GB
这个时候我们能不能每次测试的时候都去修改源码application.yml中的配置进行测试呢?显然是不行的。每次测试前改过来,每次测试后改回去,这太麻烦了。于是我们就想,需要在测试环境中创建一组临时属性,去覆盖我们源码中设定的属性,这样测试用例就相当于是一个独立的环境,能够独立测试,这样就方便多了。
临时属性
springboot已经为我们开发者早就想好了这种问题该如何解决,并且提供了对应的功能入口。在测试用例程序中,可以通过对注解@SpringBootTest添加属性来模拟临时属性,具体如下:
yml配置:
test:
prop: test code
使用properties属性临时赋值:
@SpringBootTest(properties = "test.prop=hello msg")
public class ApplicationTest {
@Value("${test.prop}")
private String msg;
@Test
public void test2() {
System.out.println(msg);
}
}
测试结果:
临时参数
除了上述这种情况,在前面讲解使用命令行启动springboot程序时讲过,通过命令行参数也可以设置属性值。而且线上启动程序时,通常都会添加一些专用的配置信息。作为运维人员他们才不懂java,更不懂这些配置的信息具体格式该怎么写,那如果我们作为开发者提供了对应的书写内容后,能否提前测试一下这些配置信息是否有效呢?当时是可以的,还是通过注解@SpringBootTest的另一个属性来进行设定。
@SpringBootTest(args = "--test.prop=test code222")
public class ApplicationTest {
@Value("${test.prop}")
private String msg;
@Test
public void test2() {
System.out.println(msg);
}
}
使用注解@SpringBootTest的args属性就可以为当前测试用例模拟命令行参数并进行测试。
好奇宝宝提问:如果两者都配置谁生效呢?.
- 答案是必然的。联想springboot配置的14种配置优先级。命令行配置是高于属性配置的(args属性配置优先于properties属性配置加载。)。
命令行的配置生效
!总结
- 加载测试临时属性可以通过注解@SpringBootTest的properties和args属性进行设定,此设定应用范围仅适用于当前测试用例
二、加载测试专用(Bean)配置
如果想要临时配置一些专用于测试环境的bean的需求,现在来解决这个问题。
- 在测试包test中创建专用的测试环境配置类
@Configuration
public class MsgConfig {
@Bean
public String msg(){
return "bean msg";
}
}
上述配置仅用于演示当前实验效果,实际开发可不能这么注入String类型的数据
2. 在启动测试环境时,导入测试环境专用的配置类,使用@Import注解即可实现
@SpringBootTest
@Import({MsgConfig.class})
public class ConfigurationTest {
@Autowired
private String msg;
@Test
void testConfiguration(){
System.out.println(msg);
}
}
到这里就通过@Import属性实现了基于开发环境的配置基础上,对配置进行测试环境的追加操作,实现了1+1的配置环境效果。这样我们就可以实现每一个不同的测试用例加载不同的bean的效果,丰富测试用例的编写,同时不影响开发环境的配置。
总结
- 定义测试环境专用的配置类,然后通过@Import注解在具体的测试中导入临时的配置,例如测试用例,方便测试过程,且上述配置不影响其他的测试类环境
三、Web环境模拟测试
在测试中对表现层功能进行测试需要一个基础和一个功能。所谓的一个基础是运行测试程序时,必须启动web环境,不然没法测试web功能。一个功能是必须在测试程序中具备发送web请求的能力,不然无法实现web功能的测试。所以在测试用例中测试表现层接口这项工作就转换成了两件事,一,如何在测试类中启动web测试,二,如何在测试类中发送web请求。下面一件事一件事进行,先说第一个
1. 测试类中启动web环境
在测试中对表现层功能进行测试需要一个基础和一个功能。所谓的一个基础是运行测试程序时,必须启动web环境,不然没法测试web功能。
每一个springboot的测试类上方都会标注@SpringBootTest注解,而注解带有一个属性,叫做webEnvironment。通过该属性就可以设置在测试用例中启动web环境,具体如下:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ApplicationTest01 {
}
测试类中启动web环境时,可以指定启动的Web环境对应的端口,springboot提供了4种设置值,分别如下:
- MOCK:根据当前设置确认是否启动web环境,例如使用了Servlet的API就启动web环境,属于适配性的配置
- DEFINED_PORT:使用自定义的端口作为web服务器端口
- RANDOM_PORT:使用随机端口作为web服务器端口
- NONE:不启动web环境
推荐使用RANDOM_PORT随机端口,避免端口被占用
2. 测试类中发送请求
对于测试类中发送请求,其实java的API就提供对应的功能,springboot为了便于开发者进行对应的功能开发,对其又进行了包装,简化了开发步骤
- 在测试类中开启web虚拟调用功能,通过注解@AutoConfigureMockMvc实现此功能的开启
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//开启虚拟MVC调用
@AutoConfigureMockMvc
public class ApplicationTest01 {
- 定义发起虚拟调用的对象MockMVC,通过自动装配的形式初始化对象
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//开启虚拟MVC调用
@AutoConfigureMockMvc
public class ApplicationTest01 {
@Test
void testWeb(@Autowired MockMvc mvc) {
}
}
这里这样写是提供了另一种注入的思路,不仅可以当做成员变量注入,也可以作为参数注入,这时只有这个方法可以使用,作为成员变量注入是当前类都可以使用
- 创建一个虚拟请求对象,封装请求的路径,并使用MockMVC对象发送对应请求
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//开启虚拟MVC调用
@AutoConfigureMockMvc
public class ApplicationTest01 {
@Test
void testWeb(@Autowired MockMvc mvc) throws Exception {
//http://localhost:8080/book
//创建虚拟请求,当前访问/book
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/book/test");
//执行对应的请求
mvc.perform(builder);
}
}
- 主机号和端口不需要写,只写访问路径。因为前面的服务器IP地址和端口使用的是当前虚拟的web环境
- MockMvcRequestBuilders可以发送多种请求(get,post,delete,put…)
- 单词mock:adj 虚拟的,假的
3. web环境请求结果比对
- 相应状态匹配
@Test
void testWeb(@Autowired MockMvc mvc) throws Exception {
//创建虚拟请求,当前访问/books
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.request(HttpMethod.GET, "/book/test1");
//执行对应的请求
ResultActions action = mvc.perform(builder);
//添加预期值,与真实值比较
StatusResultMatchers status = MockMvcResultMatchers.status();
//预期值为成功:状态码应为200
ResultMatcher ok = status.isOk();
//把本次调用的结果与预期值匹配,比较。匹配不成功将会报错
action.andExpect(ok);
}
匹配不成功,我们期望状态码为200,结果返回了404
- 响应体匹配(非json数据格式)
@Test
void testBody(@Autowired MockMvc mvc) throws Exception {
//创建虚拟请求,当前访问/books
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.request(HttpMethod.GET, "/book/test");
//执行对应的请求
ResultActions action = mvc.perform(builder);
//设定预期值 与真实值进行比较,成功测试通过,失败测试失败
//定义本次调用的预期值
ContentResultMatchers content = MockMvcResultMatchers.content();
ResultMatcher result = content.string("test");
//添加预计值到本次调用过程中进行匹配
action.andExpect(result);
}
返回结果不匹配,我们期望是“test”,实际上为“测试”
- 响应体匹配(json数据格式,开发中的主流使用方式)
@GetMapping("testJson")
public Result testJson() {
return new Result(200,"操作成功","hahahah");
}
@Test
void testJson(@Autowired MockMvc mvc) throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/book/testJson");
ResultActions action = mvc.perform(builder);
//设定预期值 与真实值进行比较,成功测试通过,失败测试失败
//定义本次调用的预期值
ContentResultMatchers content = MockMvcResultMatchers.content();
ResultMatcher result = content.json("{\"code\":200,\"msg\":\"操作失败\",\"data\":\"hahahah\"}");
//添加预计值到本次调用过程中进行匹配
action.andExpect(result);
}
匹配到msg内容不符合预期
- 响应头信息匹配
@Test
void testContentType(@Autowired MockMvc mvc) throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/book/testJson");
ResultActions action = mvc.perform(builder);
//设定预期值 与真实值进行比较,成功测试通过,失败测试失败
//定义本次调用的预期值
HeaderResultMatchers header = MockMvcResultMatchers.header();
ResultMatcher contentType = header.string("Content-Type", "application/json");
//添加预计值到本次调用过程中进行匹配
action.andExpect(contentType);
}
基本上齐了,头信息,正文信息,状态信息都有了,就可以组合出一个完美的响应结果比对结果了。以下范例就是三种信息同时进行匹配校验,也是一个完整的信息匹配过程。
@Test
void testResult(@Autowired MockMvc mvc) throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/book/testJson");
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("{\"code\":200,\"msg\":\"操作成功\",\"data\":\"hahahah\"}");
action.andExpect(result);
}
总结
- web虚拟调用可以对本地虚拟请求的返回响应信息进行比对,分为响应头信息比对、响应体信息比对、响应状态信息比对
四、数据层测试回滚
当前我们的测试程序可以完美的进行表现层、业务层、数据层接口对应的功能测试了,但是测试用例开发完成后,在打包的阶段由于
test生命周期属于必须被运行的生命周期
,如果跳过会给系统带来极高的安全隐患,所以测试用例必须执行。但是新的问题就呈现了,测试用例如果测试时产生了事务提交就会在测试过程中对数据库数据产生影响,进而产生垃圾数据
。这个过程不是我们希望发生的,作为开发者测试用例该运行运行,但是过程中产生的数据不要在我的系统中留痕,这样该如何处理呢?
springboot早就为开发者想到了这个问题,并且针对此问题给出了最简解决方案,在原始测试用例中添加注解@Transactional即可实现当前测试用例的事务不提交。当程序运行后,只要注解@Transactional出现的位置存在注解@SpringBootTest,springboot就会认为这是一个测试程序,无需提交事务,所以也就可以避免事务的提交。
@SpringBootTest
@Transactional
@Rollback(true)
public class DaoTest {
@Autowired
private BookService bookService;
@Test
void testSave(){
Book book = new Book();
book.setName("springboot3");
book.setType("springboot3");
book.setDescription("springboot3");
bookService.save(book);
}
}
- 加上@Transactional注解,默认@Rollback(true)
- 如果开发者想提交事务,也可以,再添加一个@RollBack的注解,设置回滚状态为false即可正常提交事务,是不是很方便?springboot在辅助开发者日常工作这一块展现出了惊人的能力,实在太贴心了。
总结
- 在springboot的测试类中通过添加注解@Transactional来阻止测试用例提交事务
- 通过注解@Rollback控制springboot测试类执行结果是否提交事务,需要配合注解@Transactional使用
五、测试用例数据设定
测试用例中书写的测试数据属于固定数据,往往失去了测试的意义,开发者可以针对测试用例进行针对性开发,这样就有可能出现测试用例不能完美呈现业务逻辑代码是否真实有效的达成业务目标的现象,解决方案其实很容易想,测试用例的数据只要随机产生就可以了
pringboot提供了在配置中使用随机值的机制,确保每次运行程序加载的数据都是随机的。具体如下:
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;
}
对于随机值的产生,还有一些小的限定规则,比如产生的数值性数据可以设置范围等,具体如下:
总结
- 使用随机数据可以替换测试用例中书写的固定数据,提高测试用例中的测试数据有效性