springboot-test和dbunit单元测试方案
springboot-test和dbunit单元测试方案
参考链接:Testing (spring.io)
在pom文件中添加:maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
一般的测试方法
简单的,对于springboot项目,只需要引入spring-boot-starter-test,就可以使用@SpringBootTest注解编写测试类。
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class BaseTest {
/**
* 注入发送邮件的接口
*/
@Autowired
private MailService mailService;
/**
* 测试发送文本邮件
*/
@Test
public void sendmail() {
mailService.sendSimpleMail("@qq.com", "主题:你好文本邮件", "内容:发送文本邮件");
}
@Test
public void sendmailHtml() {
mailService.sendHtmlMail("@qq.com", "主题:你好html网页邮件", "<h2>内容:第一封html网页邮件</h2>");
}
}
MockMvc测试Rest接口
除了注入service,mapper来测试方法springboot-test也提供了MockMvc来测试http请求。 MockMvc是由springboot-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。
接口MockMvcBuilder,提供一个唯一的build方法,用来构造MockMvc。主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和集成Web环境测试(并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。MockMvcBuilders提供了对应的创建方法standaloneSetup方法和webAppContextSetup方法,在使用时直接调用即可。
@Slf4j
public class CacheCtrlTest extends BaseTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();//建议使用这种
}
/**
* 1、mockMvc.perform执行一个请求。
* 2、MockMvcRequestBuilders.get("XXX")构造一个请求。
* 3、ResultActions.param添加请求传值
* 4、ResultActions.accept(MediaType.TEXT_HTML_VALUE))设置返回类型
* 5、ResultActions.andExpect添加执行完成后的断言。
* 6、ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情
* 比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
* 5、ResultActions.andReturn表示执行完成后返回相应的结果。
*/
@Test
public void cacheTest() {
try {
CacheService.KeyValue keyValue = new CacheService.KeyValue("test_cache", "测试缓存");
String requestJson = JsonUtils.toJSONString(keyValue);
//对post测试
ResultActions resultActions = mockMvc.perform(
MockMvcRequestBuilders.post("/test/testCache")
.contentType(MediaType.APPLICATION_JSON)
.characterEncoding("UTF-8")
.content(requestJson)
).andExpect(MockMvcResultMatchers.status().isOk());
resultActions.andReturn().getResponse().setCharacterEncoding("UTF-8");
resultActions.andDo(MockMvcResultHandlers.print());
//断言,判断返回的值是否正确
String content = resultActions.andReturn().getResponse().getContentAsString();
Assertions.assertEquals("{\"code\":200,\"status\":\"success\",\"message\":null,\"moreInfo\":null,\"data\":\"OK\",\"success\":true}",
content);
//对get测试
resultActions = mockMvc.perform(
MockMvcRequestBuilders.get("/test/testCache")
.contentType(MediaType.APPLICATION_JSON)
.characterEncoding("UTF-8")
.param("key", keyValue.getKey())
).andExpect(MockMvcResultMatchers.status().isOk());
//设置编码
resultActions.andReturn().getResponse().setCharacterEncoding("UTF-8");
resultActions.andDo(MockMvcResultHandlers.print());
content = resultActions.andReturn().getResponse().getContentAsString();
Assertions.assertEquals("{\"code\":200,\"status\":\"success\",\"message\":null,\"moreInfo\":null,\"data\":\"测试缓存\",\"success\":true}",
content);
CacheService.KeyValue keyValue2 = new CacheService.KeyValue("test_cache", "更新缓存");
String requestJson2 = JsonUtils.toJSONString(keyValue2);
//对put测试
MvcResult mvcResultPut = mockMvc.perform(
MockMvcRequestBuilders.put("/test/testCache")
.contentType(MediaType.APPLICATION_JSON)
.characterEncoding("UTF-8")
.content(requestJson2)
).andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();
content = mvcResultPut.getResponse().getContentAsString();
Assertions.assertEquals("{\"code\":200,\"status\":\"success\",\"message\":null,\"moreInfo\":null,\"data\":\"OK\",\"success\":true}",
content);
//对delete测试
MvcResult mvcResultDelete = mockMvc.perform(
MockMvcRequestBuilders.delete("/test/testCache")
.contentType(MediaType.APPLICATION_JSON)
.characterEncoding("UTF-8")
.param("key", keyValue.getKey())
).andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();
content = mvcResultDelete.getResponse().getContentAsString();
Assertions.assertEquals("{\"code\":200,\"status\":\"success\",\"message\":null,\"moreInfo\":null,\"data\":\"OK\",\"success\":true}",
content);
} catch (Exception e) {
log.error("缓存测试失败 e", e);
Assertions.fail("缓存测试失败", e);
}
}
}
这种测试程序运行时,依赖数据库数据。容易受到历史数据,脏数据影响。
graph LR;
构造调用-->|输入参数|pg([待测试程序])-->|输出结果|观察比较;
db[(数据库)]-->|获取数据|pg([待测试程序]);
基于dbunit+h2内存数据库的测试
dbunit是junit的一个扩展,对于数据库驱动的项目,它能在进行单元测试之前,使得数据库维持一个给定的状态。
H2是一个开源的嵌入式数据库引擎,它是一个用Java开发的类库,可直接嵌入到应用程序中,与应用程序一起打包发布,不受平台限制。
单元测试使用内存数据库时:测试类启动时会创建内存数据库,并在停止时销毁。
H2数据库地址:H2 Database Engine
graph LR;
编写测试类--->|构造输入参数|pg([待测试程序])-->|输出结果|比较Json;
准备xml格式数据-->|初始化|h2[(内存数据库)]-->|获取数据|pg([待测试程序]);
maven依赖
值得注意的是,unitils的高版本目前(2021-06-18)还未支持h2内存数据库,不要引入高版本。
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
<scope>test</scope>
</dependency>
<dependency>
<scope>test</scope>
<groupId>org.unitils</groupId>
<artifactId>unitils-dbunit</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<scope>test</scope>
<groupId>org.unitils</groupId>
<artifactId>unitils-io</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
<groupId>org.unitils</groupId>
<artifactId>unitils-database</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<scope>test</scope>
<groupId>org.unitils</groupId>
<artifactId>unitils-spring</artifactId>
<version>3.4.3</version>
</dependency>
unitls.properties
在src/test/resources/下添加unitils.properties文件。
#启用的unitils 模块
unitils.modules=database,dbunit,hibernate,spring,
#配置扩展模块
#unitils.module.dbunit.className=com.zph.programmer.springboot.utils.MySqlDbUnitModule
#配置数据库连接
database.driverClassName=org.h2.Driver
database.url=jdbc:h2:mem:test_mem:public
database.dialect=mysql
database.userName=sa
database.password=
database.schemaNames=public
# The database maintainer is disabled by default.
#数据库维护策略 在每次运行时可更新数据库 根据dbMaintainer.script.locations设置的sql文件进行更新
#当以往文件改变 将更新此文件到数据库 未改变的sql文件将不变
#命名格式 <index>_<some name>.sql
updateDataBaseSchema.enabled=true
#This table is by default not created automatically
#数据库表生成策略
dbMaintainer.autoCreateExecutedScriptsTable=true
dbMaintainer.keepRetryingAfterError.enabled=false
dbMaintainer.script.locations=./src/test/resources/sqlscript
#配置数据集工厂
DbUnitModule.DataSet.factory.default=org.unitils.dbunit.datasetfactory.impl.MultiSchemaXmlDataSetFactory
DbUnitModule.ExpectedDataSet.factory.default=org.unitils.dbunit.datasetfactory.impl.MultiSchemaXmlDataSetFactory
#配置数据库加载策略
DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.impl.CleanInsertLoadStrategy
DatabaseModule.Transactional.value.default=rollback
# XSD generator
#配置数据集结构模式XSD生成路径
dataSetStructureGenerator.xsd.dirName=resources/xsd
测试基类和配置方式
添加TestConf.java和TestApplicaiton.java
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.unitils.database.UnitilsDataSourceFactoryBean;
import javax.sql.DataSource;
@PropertySource("classpath:unitils.properties")
@TestConfiguration
public class TestConf {
//使用unitils配置的数据库
DataSource dataSource = (DataSource) new UnitilsDataSourceFactoryBean().getObject();
public TestConf() throws Exception {
}
@Bean(name = "dataSource")
public DataSource dbunitDataSource() throws Exception {
return dataSource;
}
}
如果项目使用Configuration配置的数据库,需要避免将原数据库bean实例化。这里没有使用是因为配置SQLlite数据库是,只是配置了properties,借用springboot的约定,没有显示指定dataesource时,springboot会默认读取配置创建。上面TestConf 在测试配置中指定了dbunits配置的数据库。
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import java.io.IOException;
@Slf4j
@ComponentScan(basePackages = "com.zph.programmer.springboot"
, excludeFilters = {
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = TestApplication.TypeExFilter.class)
})
public class TestApplication {
/**
* 避免以下的Bean被实例化
*/
public static class TypeExFilter implements TypeFilter {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
String n = metadataReader.getClassMetadata().getClassName();
/* if (n.equals(DataSourceConf.class.getName())) {
return true;
}
*/
return false;
}
}
}
TestRunner.java
import org.junit.internal.runners.InitializationError;
import org.junit.runner.notification.RunNotifier;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.unitils.UnitilsJUnit4TestClassRunner;
public class TestRunner extends UnitilsJUnit4TestClassRunner {
private final SpringRunner springRunner;
public TestRunner(Class<?> clazz) throws org.junit.runners.model.InitializationError, InitializationError {
super(clazz);
springRunner = new SpringRunner(clazz);
}
@Override
public void run(RunNotifier notifier) {
super.run(notifier);
}
@Override
protected Object createTest() throws Exception {
//使用SpringJUnit4ClassRunner.createTest(), 兼容@Bean @Autowired
return springRunner.createTest();
}
public static class SpringRunner extends SpringJUnit4ClassRunner {
public SpringRunner(Class<?> clazz) throws org.junit.runners.model.InitializationError {
super(clazz);
}
@Override
public Object createTest() throws Exception {
return super.createTest();
}
}
}
基类BaseUnitilsTest.java
@RunWith(TestRunner.class)
@SpringBootTest(classes = {TestConf.class, TestApplication.class},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("utfake")
@Slf4j
public class BaseUnitilsTest {
@Before
public void before() {
MockitoAnnotations.initMocks(this);
}
@After
public void after() {
ReflectTestUtils.revert();
}
@Ignore
@Test
public void t() {
}
}
使用方式
xml文件准备REST_CALL_LOG_RECORD表数据:
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<REST_CALL_LOG_RECORD id="13" method="GET" uri="http://127.0.0.1:8180/v2/api-docs" request=""
response="" status="200" cost_time="385" is_valid="1" created_time="2021-06-14 16:44:03"
modified_time="2021-06-14 16:44:03"/>
</dataset>
@DataSet:测试之前初始化数据库
@ExpectedDataset:测试之后比对数据库数据
另外:JsonComparator是第三方工具类,读取json文件比较结果。
@Slf4j
public class TestServiceTest extends BaseUnitilsTest {
@Resource
private TestService testService;
/**
* dbunit need junit4 test
*/
@Test
@DataSet(value = {"rest_call_log_record.init.xml"})
@ExpectedDataSet(value = {"rest_call_log_record.init.xml"})
public void findRestLogById() {
RestCallLogRecord record = testService.findRestLogById(13);
JsonComparator.newInstance("findRestLogById.expected.json").compareAssert(record);
}
}
留下评论