关于测试这个话题,小伙伴们都很熟悉,在具体展开前,我们先来看一个图(关于经典软件工程阶段,与互联网软件交付阶段)
在经典软件工程阶段中,有编码测试阶段,说明了测试的重要性;
在互联网软件交付阶段中,没有直接看到测试相关的字眼,但是有生产就绪阶段,下面我们把生产就绪展开来看
在生产就绪checklist中,我们看到
- 功能测试ok
- 性能测试ok
不言而喻说明了测试的重要性。尤其在云原生微服务架构体系实践中,实践好CI/CD、DevOps的前置条件,测试至关重要!
那么今天,顺接上一篇分享文章:云原生微服务架构体系实践思考,与你共同探讨关于微服务测试设计的一些思考,并期望在生产实践中带来一些帮助。
1.测试金字塔
当我们谈到测试的时候,传统意义上我们都知道软件测试需要
- 单元测试
- 集成测试
- 用户测试
但是细分的话,估计很多小伙伴不一定能明确它们之间的边界。下面我们从应用分层架构开始谈起,一个基于mvc分层架构应用,大概是这样的
上图是典型的三层应用架构
- controller
- service
- dao
同时还涉及到服务之间调用,以及引用外部存储服务。我们需要考虑服务内部各层、服务之间、外部依赖服务以至于整个链路的测试ok。于是业界提出了测试金字塔的理论模型,何为测试金字塔呢?看一个图
我们看到整个测试阶段分为
- 单元测试
- 集成测试
- 组件测试
- 端到端测试
- 探索测试
单元测试比较好理解,比如说针对每个controller、service、dao类及方法的测试
集成测试与组件测试,在实践中可以作为一个阶段,比如开发好某个功能后,通过postman工具调用测试(前后端联调)
端到端测试,即测试小伙伴,产品、用户进行验证测试了
探索测试,即构建一些自动化的测试(比较高级)
2.构建单元测试案例
理解测试金字塔体系以后,最重要的需要在实际项目中实践起来,才显得有意义!精通的目的全在于应用!
下面我们以一个springboot应用为例,看如何构建相关的单元测试用例。这个对于研发小伙伴来说很重要,我们在开发好每个功能每个类,原则上都应该要配备相关的单元测试类,如此能为我们的交付质量保驾护航!
2.1.整体应用结构
2.2.关键环境准备说明
基于springboot的应用,实践单元测试非常容易,环境方面准备
- 引入:spring-boot-starter-test依赖
- 注解:@RunWith
- 注解:@SpringBootTest
2.2.1.引入依赖
引入test依赖的同时,引入了h2数据库依赖,h2是内存数据库,用于解除执行单元测试用例时,对外部存储mysql的依赖
<!--test 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--内存数据库h2依赖-->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
2.2.2.测试基类BaseTester
/**
* 测试基类
* 构建命令:mvn clean package
* 跳过单元测试:
* mvn clean package -DskipTests
* mvn clean package -Dmaven.test.skip=true
* @author ThinkPad
* @version 1.0
* @date 2021/10/4 16:45
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class BaseTester {
}
2.2.3.Web测试基类WebBaseTester
执行controller层单元测试用例,需要相关的一些mock,解除对外部服务的依赖,注意类上面的webEnvironment配置,以及@AutoConfigureMockMvc注解
/**
* web测试基类
*
* @author ThinkPad
* @version 1.0
* @date 2021/10/4 16:55
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@AutoConfigureMockMvc
public class WebBaseTester extends BaseTester{
}
2.2.4.编写测试专用配置
为了解除对外部服务的依赖,或者外部环境的依赖,比如
- 外部存储mysql,通过内存数据库h2
因此,需要准备特定的测试配置文件,下面关注数据源datasouce配置
spring:
application:
name: follow-me-springboot-test
datasource:
url: jdbc:h2:mem:account;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MYSQL
username: sa
password:
driver-class-name: org.h2.Driver
continue-on-error: false
platform: h2
schema: classpath:/db/schema.sql
h2:
console:
enabled: true
jpa:
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate:
format_sql: true
#feign配置
feign:
client:
config:
default:
loggerLevel: basic
#日志配置
logging:
level:
cn.edu.anan: debug
2.3.相关测试用例
2.3.1.controller单元测试用例
/**
* controller层测试类
* mockito框架使用参考:https://segmentfault.com/a/1190000006746409
* @author ThinkPad
* @version 1.0
* @date 2021/10/3 16:52
*/
@Slf4j
public class BookControllerTest extends WebBaseTester{
@Autowired
MockMvc mockMvc;
@MockBean
private AccountClient accountClient;
/**
* 初始化测试数据
*/
@Autowired
private BookController bookController;
@Before
public void setUp(){
Book book = Book.builder()
.id(1)
.author("xy")
.bookName("java")
.publishDate(LocalDate.now())
.build();
Book book2 = Book.builder()
.id(2)
.author("lj")
.bookName("spring")
.publishDate(LocalDate.now())
.build();
bookController.save(book);
bookController.save(book2);
}
/**
* 测试查询图书列表
* @throws Exception
*/
@Test
public void testListBook() throws Exception{
// 查询结果
Account account = Account.builder()
.id("666")
.name("xy")
.email("xy@126.com")
.build();
// mock
when(accountClient.findById(anyString()))
.thenReturn(account);
// mock查询图书列表
MvcResult mvcResult = mockMvc.perform(get("/book/list"))
.andExpect(status().isOk())
.andReturn();
String contentAsString = mvcResult.getResponse().getContentAsString();
log.info("测试结果:{}", contentAsString);
}
}
2.3.2.service单元测试用例
/**
* service层测试
*
* @author ThinkPad
* @version 1.0
* @date 2021/10/3 16:12
*/
@Slf4j
public class AccountServiceTest extends BaseTester{
@Autowired
private AccountService accountService;
@Test
public void testService(){
// 准备数据
Account account = Account.builder()
.name("yhh")
.email("yhh@126.com")
.build();
// 保存
accountService.save(account);
// 查询
Account accountById = accountService.findAccountById(account.getId());
log.info("查询到数据:{}", accountById);
}
}
2.3.3.dao单元测试用例
/**
* dao层单元测试
*
* @author ThinkPad
* @version 1.0
* @date 2021/10/3 15:41
*/
@Slf4j
@FixMethodOrder(value = MethodSorters.NAME_ASCENDING)
public class AccountDaoTest extends BaseTester{
@Autowired
private AccountDao accountDao;
private Account account;
/**
* 初始化数据
*/
@Before
public void setUp() {
log.info(".................setUp..................");
// 准备账户数据
account = Account.builder()
.name("yhh")
.email("yhh@126.com")
.build();
}
/**
* 保存数据
*/
@Test
public void test001Save(){
log.info(".................test001Save..................");
accountDao.save(account);
}
/**
* 查询列表数据
*/
@Test
public void test002ListAccount(){
log.info(".................test002ListAccount..................");
// 查询
Iterable<Account> all = accountDao.findAll();
all.forEach(acc ->{
log.info("查询到数据:{}", acc);
});
}
}
写在最后,有了上面的单元测试用例
- 当我们在构建项目的时候,会选择执行相关的单元测试用例,只有测试用例都执行成功,才能完成构建
- 或者配合CI/CD流水线,只有单元测试用例都执行通过的时候,才允许将代码合并到主干master分支,进一步构建完成发布部署
这即是我们今天这篇文章分享最重要的一个点,另外本文完整源码需要的小伙伴,可以参考代码仓库:gitee.com/yanghouhua/…,其中的follow-me-springboot-test模块