SpringBoot——测试
SpringBoot测试
源码在test-springboot
测试无非就是设定预期值与真实值比较,相同则测试通过,不同则测试失败
Ctrl+鼠标左键看源码,再按ctrl:f12查看方法
0、环境
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<groupId>com.lmcode</groupId>
<artifactId>lmcode</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package com.lmcode;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/** 可以通过args传入一些属性进行临时配置 */
@SpringBootApplication
public class TestSpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(TestSpringbootApplication.class, args);
}
}
test.prop=testValue
server.port=9090
1、当前测试类专用属性
在小范围测试环境(例如当前测试类)有效,比多环境开发中的测试环境影响范围更小;
package com.lmcode;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
/**
* 只在当前测试类中有效,加一个临时属性:properties
* */
@SpringBootTest(properties = {"test.prop=testValue---properties"})
public class demo1_PropertiesTests {
@Value("${test.prop}")
private String msg;
@Test
public void testProperties(){
System.out.println(msg);
}
}
package com.lmcode;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
/**
* 只在当前测试类中有效,加一个临时属性:args
* 优先级币properties高
* 相对于在idea工具中写参数的好处是配置一直存在,不会因为换平台而消失
* */
@SpringBootTest(properties = {"test.prop=testValue---properties"},
args = {"--test.prop=testValue---args"})
public class demo2_ArgsTests {
@Value("${test.prop}")
private String msg;
@Test
void testArgs(){
System.out.println(msg);
}
}
2、 当前测试类专用配置
使用@Import
加载当前测试类专用配置
package com.lmcode.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MsgConfig {
@Bean
public String msg(){
return "bean---msg";
}
}
package com.lmcode;
import com.lmcode.config.MsgConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
/**
* 局部的配置导入,仅在当前测试类生效
* 临时加载bean,服务于测试【不能下载源码(main)中】
* */
@SpringBootTest
@Import({MsgConfig.class})
public class demo3_ConfigurationTests {
@Autowired
private String msg;
@Test
void testConfiguration(){
System.out.println(msg);
}
}
3、模拟Web环境测试表现层
可以匹配Status、Error message、Headers、Content type、Body、Forwarded URL、Redirected URL、Cookies等都可以匹配
package com.lmcode.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/books")
public class BookController {
@GetMapping
public String getById(){
System.out.println("getById run success");
return "success";
}
}
3.1 测试类中启动web环境,发送虚拟请求并匹配响应执行状态
package com.lmcode;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.result.StatusResultMatchers;
/**
* 属性是枚举类,其中RANDOM_PORT是随机端口
* 可以通过把请求地址故意写错看预计不通过的控制台输出
* */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 开启虚拟调用
@AutoConfigureMockMvc
public class demo4_WebTest {
@Test
void testWeb(@Autowired MockMvc mockMvc) throws Exception {
// 模拟虚拟的http请求 并 执行
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
mockMvc.perform(builder);
}
@Test
void testStatus(@Autowired MockMvc mockMvc) throws Exception {
// 模拟虚拟的http请求 并 执行
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
ResultActions action = mockMvc.perform(builder);
// 设定预期值
StatusResultMatchers status = MockMvcResultMatchers.status();
ResultMatcher ok = status.isOk();
// 对比真实值,添加预计值到本次调用过程进行匹配 并 断言
action.andExpect(ok);
}
}
3.2 匹配响应体
package com.lmcode;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.ContentResultMatchers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.result.StatusResultMatchers;
/**
* 属性是枚举类,其中RANDOM_PORT是随机端口
* 可以通过把请求地址故意写错看预计不通过的控制台输出
* */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 开启虚拟调用
@AutoConfigureMockMvc
public class demo4_WebTest {
@Test
void testBody(@Autowired MockMvc mockMvc) throws Exception {
// 模拟虚拟的http请求 并 执行
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
ResultActions action = mockMvc.perform(builder);
// 设定预期值
ContentResultMatchers content = MockMvcResultMatchers.content();
ResultMatcher result = content.string("success");
// 对比真实值,添加预计值到本次调用过程进行匹配 并 断言
action.andExpect(result);
}
}
3.3 匹配响应体(json)
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
package com.lmcode.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private int id;
private String name;
private String type;
private String description;
}
package com.lmcode.controller;
import com.lmcode.domain.Book;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/books")
public class BookController {
@GetMapping
public String getById(){
System.out.println("getById run success");
return "success";
}
@GetMapping("/getAll")
public Book getAll(){
Book book = new Book();
book.setId(1);
book.setName("SpringBoot");
book.setType("technology");
book.setDescription("I'm No.1");
System.out.println("getAll run success");
return book;
}
}
package com.lmcode;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.ContentResultMatchers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.result.StatusResultMatchers;
/**
* 属性是枚举类,其中RANDOM_PORT是随机端口
* 可以通过把请求地址故意写错看预计不通过的控制台输出
* */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 开启虚拟调用
@AutoConfigureMockMvc
public class demo4_WebTest {
@Test
void testWeb(@Autowired MockMvc mockMvc) throws Exception {
// 模拟虚拟的http请求 并 执行
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
mockMvc.perform(builder);
}
@Test
void testStatus(@Autowired MockMvc mockMvc) throws Exception {
// 模拟虚拟的http请求 并 执行
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
ResultActions action = mockMvc.perform(builder);
// 设定预期值
StatusResultMatchers status = MockMvcResultMatchers.status();
ResultMatcher ok = status.isOk();
// 对比真实值,添加预计值到本次调用过程进行匹配 并 断言
action.andExpect(ok);
}
@Test
void testBody(@Autowired MockMvc mockMvc) throws Exception {
// 模拟虚拟的http请求 并 执行
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/getAll");
ResultActions action = mockMvc.perform(builder);
// 设定预期值
ContentResultMatchers content = MockMvcResultMatchers.content();
ResultMatcher result = content.json("{\n" +
" \"id\": 1,\n" +
" \"name\": \"SpringBoot\",\n" +
" \"type\": \"technology\",\n" +
" \"description\": \"I'm No.1\"\n" +
"}");
// 对比真实值,添加预计值到本次调用过程进行匹配 并 断言
action.andExpect(result);
}
}
3.4 匹配响应头
package com.lmcode;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.ContentResultMatchers;
import org.springframework.test.web.servlet.result.HeaderResultMatchers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.result.StatusResultMatchers;
/**
* 属性是枚举类,其中RANDOM_PORT是随机端口
* 可以通过把请求地址故意写错看预计不通过的控制台输出
* */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 开启虚拟调用
@AutoConfigureMockMvc
public class demo4_WebTest {
@Test
void testWeb(@Autowired MockMvc mockMvc) throws Exception {
// 模拟虚拟的http请求 并 执行
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
mockMvc.perform(builder);
}
@Test
void testStatus(@Autowired MockMvc mockMvc) throws Exception {
// 模拟虚拟的http请求 并 执行
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
ResultActions action = mockMvc.perform(builder);
// 设定预期值
StatusResultMatchers status = MockMvcResultMatchers.status();
ResultMatcher ok = status.isOk();
// 对比真实值,添加预计值到本次调用过程进行匹配 并 断言
action.andExpect(ok);
}
@Test
void testBody(@Autowired MockMvc mockMvc) throws Exception {
// 模拟虚拟的http请求 并 执行
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/getAll");
ResultActions action = mockMvc.perform(builder);
// 设定预期值
ContentResultMatchers content = MockMvcResultMatchers.content();
ResultMatcher result = content.json("{\n" +
" \"id\": 1,\n" +
" \"name\": \"SpringBoot\",\n" +
" \"type\": \"technology\",\n" +
" \"description\": \"I'm No.1\"\n" +
"}");
// 对比真实值,添加预计值到本次调用过程进行匹配 并 断言
action.andExpect(result);
}
@Test
void testContentType(@Autowired MockMvc mockMvc) throws Exception {
// 模拟虚拟的http请求 并 执行
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/getAll");
ResultActions action = mockMvc.perform(builder);
// 设定预期值
HeaderResultMatchers header = MockMvcResultMatchers.header();
ResultMatcher result = header.string("Content-Type","application/json");
// 对比真实值,添加预计值到本次调用过程进行匹配 并 断言
action.andExpect(result);
}
}
3.5 测试多匹配【规范】
package com.lmcode;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.ContentResultMatchers;
import org.springframework.test.web.servlet.result.HeaderResultMatchers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.result.StatusResultMatchers;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class demo5_WebTestFinal {
@Test
void getAll(@Autowired MockMvc mockMvc) throws Exception {
// 模拟虚拟的http请求 并 执行
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/getAll");
// 设定预期值
ResultActions action = mockMvc.perform(builder);
// status
StatusResultMatchers status = MockMvcResultMatchers.status();
ResultMatcher ok = status.isOk();
action.andExpect(ok);
// body
ContentResultMatchers content = MockMvcResultMatchers.content();
ResultMatcher result = content.json("{\n" +
" \"id\": 90901,\n" +
" \"name\": \"SpringBoot\",\n" +
" \"type\": \"technology\",\n" +
" \"description\": \"I'm No.1\"\n" +
"}");
action.andExpect(result);
// contentType
HeaderResultMatchers header = MockMvcResultMatchers.header();
ResultMatcher contentType = header.string("Content-Type","application/json");
action.andExpect(contentType);
}
}
4、业务层测试事物回滚
为测试用例添加事物,SpringBoot会对测试用例对应的事物提交操作进行回滚
如皋要提交事物,可以通过
@Rollback
配置
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
spring:
datasource:
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mini_springmvc?serverTimezone=UTC
username: root
password: 1234
mybatis-plus:
global-config:
db-config:
table-prefix: tb_
id_type: auto
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
package com.lmcode.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private Integer id;
private String name;
private String type;
private String description;
}
package com.lmcode.controller;
import com.lmcode.domain.Book;
import com.lmcode.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/books")
public class BookController {
@GetMapping
public String getById(){
System.out.println("getById run success");
return "success";
}
@GetMapping("/getAll")
public Book getAll(){
Book book = new Book();
book.setId(90901);
book.setName("SpringBoot");
book.setType("technology");
book.setDescription("I'm No.1");
System.out.println("getAll run success");
return book;
}
@Autowired
private BookService bookService;
@PostMapping("/save")
public Boolean save(@RequestBody Book book){
return bookService.save(book);
}
}
package com.lmcode.service;
import com.lmcode.domain.Book;
public interface BookService {
boolean save(Book book);
}
package com.lmcode.service.impl;
import com.lmcode.domain.Book;
import com.lmcode.mapper.BookMapper;
import com.lmcode.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookMapper bookMapper;
@Override
public boolean save(Book book) {
return bookMapper.insert(book) > 0;
}
}
package com.lmcode.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lmcode.domain.Book;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BookMapper extends BaseMapper<Book> {
}
package com.lmcode;
import com.lmcode.domain.Book;
import com.lmcode.service.BookService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
/** spring会自动识别,在测试类加事物自动回滚*/
@SpringBootTest
@Transactional
@Rollback(value = false)
//@Rollback(value = true)
public class demo6_ServiceTest {
@Autowired
private BookService bookService;
@Test
void testSave(){
Book book = new Book();
book.setName("傲慢与偏见");
book.setType("文学类");
book.setDescription("你要放下你的偏见");
bookService.save(book);
}
}
5、测试用例设置随机数据
测试用例数据通常采用随机值进行测试,可以通过配置使用SpringBoot提供的随机数为其赋值
spring:
datasource:
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mini_springmvc?serverTimezone=UTC
username: root
password: 1234
mybatis-plus:
global-config:
db-config:
table-prefix: tb_
id_type: auto
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# int,value(String),uuid(String),long(long,可以存时间)
testcase:
book:
id: ${random.int(90900,10000)}
name: ${random.value}
type: ${random.value}
description: 描述${random.value}
package com.lmcode.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
// 未配置Spring Boot 配置注解处理器
@ConfigurationProperties(prefix = "testcase.book")
public class Book {
private Integer id;
private String name;
private String type;
private String description;
}
package com.lmcode;
import com.lmcode.domain.Book;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class demo7_UseTestCase {
@Autowired
private Book book;
@Test
void testCase(){
System.out.println(book);
/* md5加密字符串,32位
Book(id=-552308655,
name=ab6939419e9dbcb95d679d185d9595ea,
type=70d0625bf03e57a3e8b7e312b0ac865c,
description=描述be4dee034654bd1ecaab738073459fa2)
*/
}
}
<!--配置Spring Boot 配置注解处理器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>