SpringBoot开发实用篇之测试

本文最后更新于:13 天前

使用注解@SpringBootTest的properties属性可以为当前测试用例添加临时的属性,覆盖源码配置文件中对应的属性值进行测试。

加载测试临时属性可以通过注解@SpringBootTest的properties和args属性进行设定,此设定应用范围仅适用于当前测试用例。

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

在测试类中测试web层接口要保障测试类启动时启动web容器,使用@SpringBootTest注解的webEnvironment属性可以虚拟web环境用于测试;为测试方法注入MockMvc对象,通过MockMvc对象可以发送虚拟请求,模拟web请求调用过程。

在springboot的测试类中通过添加注解@Transactional来阻止测试用例提交事务。通过注解@Rollback控制springboot测试类执行结果是否提交事务,需要配合注解@Transactional使用。

加载测试专用属性

临时属性

在测试用例程序中,可以通过对注解@SpringBootTest添加属性来模拟临时属性,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
//properties属性可以为当前测试用例添加临时的属性配置
@SpringBootTest(properties = {"test.prop=testValue"})
public class PropertiesAndArgsTest {

@Value("${test.prop}")
private String msg;

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

使用注解@SpringBootTest的properties属性可以为当前测试用例添加临时的属性,覆盖源码配置文件中对应的属性值进行测试。

临时参数

在前面使用命令行启动springboot程序时,通过命令行参数也可以设置属性值。而且线上启动程序时,通常都会添加一些专用的配置信息。作为开发者能否提前测试一下这些配置信息是否有效呢?当然是可以的,还是通过注解@SpringBootTest的另一个属性来进行设定。

1
2
3
4
5
6
7
8
9
10
11
12
//args属性可以为当前测试用例添加临时的命令行参数
@SpringBootTest(args={"--test.prop=testValue2"})
public class PropertiesAndArgsTest {

@Value("${test.prop}")
private String msg;

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

使用注解@SpringBootTest的args属性就可以为当前测试用例模拟命令行参数并进行测试。


如果两者共存呢?思考一下配置属性与命令行参数的加载优先级,这个结果就不言而喻了。在属性加载的优先级设定中,有明确的优先级设定顺序:

在属性加载优先级的顺序中,明确规定了命令行参数的优先级排序是11,而配置属性的优先级是3,结果不言而喻了,args属性配置优先于properties属性配置加载。

加载测试专用配置

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

步骤①:在测试包test中创建专用的测试环境配置类

1
2
3
4
5
6
7
@Configuration
public class MsgConfig {
@Bean
public String msg(){
return "bean msg";
}
}

上述配置仅用于演示当前实验效果,实际开发可不能这么注入String类型的数据


步骤②:在启动测试环境时,导入测试环境专用的配置类,使用@Import注解即可实现

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
@Import({MsgConfig.class})
public class ConfigurationTest {

@Autowired
private String msg;

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

通过@Import属性实现了基于开发环境的配置基础上,对配置进行测试环境的追加操作,实现了1+1的配置环境效果。这样我们就可以实现每一个不同的测试用例加载不同的bean的效果,丰富测试用例的编写,同时不影响开发环境的配置。

Web环境模拟测试

测试表现层接口时,必须启动web环境,在测试程序中具备发送web请求的能力。

测试类中启动web环境

每一个springboot的测试类上方都会标准@SpringBootTest注解,该注解带有一个属性webEnvironment。通过webEnvironment属性可以设置在测试用例中启动web环境

1
2
3
@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环境

通过上述配置,现在启动测试程序时就可以正常启用web环境了,建议大家测试时使用RANDOM_PORT,避免代码中因为写死设定引发线上功能打包测试时由于端口冲突导致意外现象的出现。

测试类中发送请求

对于测试类中发送请求,其实java的API就提供对应的功能。springboot为了便于开发者进行对应的功能开发,对其又进行了包装,简化了开发步骤,具体操作如下:

步骤①:在测试类中开启web虚拟调用功能,通过注解@AutoConfigureMockMvc实现此功能的开启

1
2
3
4
5
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//开启虚拟MVC调用
@AutoConfigureMockMvc
public class WebTest {
}

步骤②:定义发起虚拟调用的对象MockMVC,通过自动装配的形式初始化对象

1
2
3
4
5
6
7
8
9
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//开启虚拟MVC调用
@AutoConfigureMockMvc
public class WebTest {

@Test
void testWeb(@Autowired MockMvc mvc) {
}
}

步骤③:创建一个虚拟请求对象,封装请求的路径,并使用MockMVC对象发送对应请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//开启虚拟MVC调用
@AutoConfigureMockMvc
public class WebTest {

@Test
void testWeb(@Autowired MockMvc mvc) throws Exception {
//http://localhost:8080/books
//创建虚拟请求,当前访问/books
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
//执行对应的请求
mvc.perform(builder);
}
}

执行测试程序,现在就可以正常的发送/books对应的请求了:


总结

  1. 在测试类中测试web层接口要保障测试类启动时启动web容器,使用@SpringBootTest注解的webEnvironment属性可以虚拟web环境用于测试;
  2. 为测试方法注入MockMvc对象,通过MockMvc对象可以发送虚拟请求,模拟web请求调用过程。

web环境请求结果比对

目前已经成功的发送了请求,但是还没有起到测试的效果,测试过程必须出现预计值与真实值的比对结果才能确认测试结果是否通过,虚拟请求中能对哪些请求结果进行比对呢?

发完请求得到的信息只有一种,就是响应对象。web虚拟调用可以对本地虚拟请求的返回响应信息进行比对,分为响应头信息比对、响应体信息比对、响应状态信息比对。

  • 响应状态匹配

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RestController
    @RequestMapping("/books")
    public class BookController {
    @GetMapping
    public String getById(){
    System.out.println("getById is Running……");
    return "springboot";
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Test
    void testStatus(@Autowired MockMvc mvc) throws Exception {
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books1");
    ResultActions action = mvc.perform(builder);

    //设定预期值 与真实值进行比较,成功测试通过,失败测试失败
    //定义执行状态匹配器
    StatusResultMatchers status = MockMvcResultMatchers.status();
    //预计本次调用时成功的预期值:状态200
    ResultMatcher ok = status.isOk();
    //添加预计值到本次调用过程中进行匹配
    action.andExpect(ok);
    }

  • 响应体匹配(非json数据格式)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RestController
    @RequestMapping("/books")
    public class BookController {
    @GetMapping
    public String getById(){
    System.out.println("getById is Running……");
    return "springboot";
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Test
    void testBody(@Autowired MockMvc mvc) throws Exception {
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
    ResultActions action = mvc.perform(builder);

    //设定预期值 与真实值进行比较,成功测试通过,失败测试失败
    //定义执行结果匹配器
    ContentResultMatchers content = MockMvcResultMatchers.content();
    //定义预期执行结果
    ResultMatcher result = content.string("sprngboot1");
    //添加预计值到本次调用过程中进行匹配
    action.andExpect(result);
    }

  • 响应体匹配(json数据格式,开发中的主流使用方式)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @RestController
    @RequestMapping("/books")
    public class BookController {

    @GetMapping
    public Book getById() {
    System.out.println("getById is Running……");
    Book book = new Book();
    book.setId(1);
    book.setName("springboot");
    book.setType("springboot");
    book.setDescription("springboot");
    return book;
    }

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Test
    void testJson(@Autowired MockMvc mvc) throws Exception {
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
    ResultActions action = mvc.perform(builder);
    //设定预期值 与真实值进行比较,成功测试通过,失败测试失败
    //定义执行结果匹配器
    ContentResultMatchers content = MockMvcResultMatchers.content();
    //定义预期执行结果
    ResultMatcher result = content.json("{\"id\":1,\"name\":\"SpringBoot2\"}");
    //使用本次真实执行结果与预期结果进行比对
    action.andExpect(result);
    }

  • 响应头信息匹配

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @RestController
    @RequestMapping("/books")
    public class BookController {

    @GetMapping
    public Book getById() {
    System.out.println("getById is Running……");
    Book book = new Book();
    book.setId(1);
    book.setName("springboot");
    book.setType("springboot");
    book.setDescription("springboot");
    return book;
    }

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Test
    void testContentType(@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","text/plain;charset=UTF-8");
    //添加预计值到本次调用过程中进行匹配
    action.andExpect(contentType);
    }

头信息,正文信息,状态信息都有了,就可以组合出一个完美的响应结果比对结果了。以下范例就是三种信息同时进行匹配校验,也是一个完整的信息匹配过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
void testGetById(@Autowired MockMvc mvc) throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
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("{\"id\":1,\"name\":\"springboot\",\"type\":\"springboot\"}");
action.andExpect(result);
}

数据层测试回滚

当前我们的测试程序可以完美的进行表现层、业务层、数据层接口对应的功能测试了,但是测试用例开发完成后,在打包的阶段由于test生命周期属于必须被运行的生命周期,如果跳过会给系统带来极高的安全隐患,所以测试用例必须执行。但是新的问题就呈现了,测试用例如果测试时产生了事务提交就会在测试过程中对数据库数据产生影响,进而产生垃圾数据。这个过程不是我们希望发生的,作为开发者测试用例该运行运行,但是过程中产生的数据不要在我的系统中留痕,这样该如何处理呢?

springboot早就为开发者想到了这个问题,并且针对此问题给出了最简解决方案。在原始测试用例中添加注解@Transactional即可实现当前测试用例的事务不提交。当程序运行后,只要注解@Transactional出现的位置存在注解@SpringBootTest,springboot就会认为这是一个测试程序,无需提交事务,所以也就可以避免事务的提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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);
}
}

如果此时开发者想提交事务,可以再添加一个@RollBack的注解,设置回滚状态为false即可正常提交事务。


总结

  • 在springboot的测试类中通过添加注解@Transactional来阻止测试用例提交事务。
  • 通过注解@Rollback控制springboot测试类执行结果是否提交事务,需要配合注解@Transactional使用。

测试用例数据设定

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

1
2
3
4
5
6
7
8
testcase:
book:
id: ${random.int}
id2: ${random.int(10)}
type: ${random.int!5,10!}
name: ${random.value}
uuid: ${random.uuid}
publishTime: ${random.long}

当前配置就可以在每次运行程序时创建一组随机数据,避免每次运行时数据都是固定值的尴尬现象发生,有助于测试功能的进行。数据的加载按照之前加载数据的形式,使用@ConfigurationProperties注解即可:

1
2
3
4
5
6
7
8
9
10
11
@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;
}
1
2
3
4
5
6
7
8
9
10
@SpringBootTest
public class PropertiesAndArgsTest {

@Autowired
private BookCase bookCase;

@Test
void testProperties(){ System.out.println(bookCase); }

}


对于随机值的产生,还有一些小的限定规则,比如产生的数值性数据可以设置范围等,具体如下:

${random.int(10,20)}:表示10到20的随机数,其中的分隔符 () 可以是任意字符,例如 []!! 均可。