当前位置: 首页 > web >正文

小架构step系列12:单元测试

1 概述

测试的种类很多:单元测试、集成测试、系统测试等,程序员写代码进行测试的可以称为白盒测试,单元测试和集成测试都可以进行白盒测试,可以理解为单元测试是对某个类的某个方法进行测试,集成测试则是测试一连串的方法。测试代码也是代码,即使要求测试代码逻辑简单,但写单元测试代码也是要花时间的,维护也是要花时间的,所以在写测试代码上也要有些平衡。单元测试的颗粒度比较小,最接近方法的业务逻辑,应该详尽的测试;而集成测试牵扯的方法比较多,就比较复杂,就挑重点的地方做测试。本文先了解单元测试。

2 单元测试

2.1 业务逻辑测试

在DDD里有一种思想,就是要把业务逻辑分离出来,这部分业务逻辑是业务的核心,应该是公司的核心价值所在,所以应该进行详尽测试。测试要做得多,成本就要低,所以这块的测试主要集中在逻辑方面,业务逻辑的代码也要写成函数的形式,也就是给予一定的参数,就会反馈相同的结果。这块代码不应该跟数据库、中间件等扯上关系,否则就很难做到轻量化。

有不少业务逻辑代码是比较简单的,不一定会按DDD的模式进行文件或者目录分离,但也要在类或者方法层面做到把业务逻辑分离,以便对这些业务进行良好的单元测试。这种测试仅需要对一些类进行mock,然后就跟测试一个有参数有返回值的方法那样简单。这就要求涉及到数据库或者中间件等外部资源的操作,都应该接口化,这样就比较容易进行mock。

2.2 测试例子

假设有个创建组成员的功能,为这个业务逻辑创建一个服务接口GroupMemberCreator和服务类GroupMemberCreatorImpl,里面有个方法create(GroupMember member),用于编写创建成员的业务逻辑,有部分业务会封装到业务对象GroupMember中;这个方法里需要把数据存储到数据库,则通过GroupMemberRepository提供save()接口把数据存储到数据库中,该接口只有数据库相关操作,而没有业务逻辑。

// 把业务逻辑放到GroupMember和GroupMemberCreatorImpl里
public class GroupMember {private Long groupId;private String name;public GroupMember(Long groupId, String name) {// 做业务逻辑:校验参数并组装GroupMember信息this.groupId = groupShouldExist(groupId);this.name = memberNameShouldNotExist(name);}public Long getGroupId() {return groupId;}public String getName() {return name;}private Long groupShouldExist(Long groupId) {// 校验组存在,否则抛异常return groupId;}private String memberNameShouldNotExist(String name) {// 校验成员是否已经存在,否则抛异常return name;}
}public interface GroupMemberCreator {GroupMember create(Long groupId, String memberName);
}
@Service
public class GroupMemberCreatorImpl implements GroupMemberCreator {private GroupMemberRepository repository;@Autowiredpublic GroupMemberCreatorImpl(GroupMemberRepository repository) {this.repository = repository;}@Overridepublic GroupMember create(Long groupId, String memberName) {GroupMember member = new GroupMember(groupId, memberName);return repository.save(member);}
}// Repository只有数据库相关操作,无业务逻辑
public interface GroupMemberRepository {GroupMember save(GroupMember member);
}
@Repository
public class GroupMemberReposistoryImpl implements GroupMemberRepository {@Overridepublic GroupMember save(GroupMember member) {// 调相关DAO操作数据库return member;}
}// 测试用例例子
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.ArgumentMatchers.any;
public class GroupMemberCreatorImplTest {@Testpublic void create_new_member() {Long groupId = 1L;String name = "Joy";// 1. mock对象// 调repository.save()的时候,返回一个mock对象,不真正调用数据库GroupMember mockMember = new GroupMember(groupId, name);GroupMemberRepository repository = Mockito.mock(GroupMemberRepository.class);Mockito.when(repository.save(any())).thenReturn(mockMember);// 2. 测试代码逻辑GroupMemberCreatorImpl creator = new GroupMemberCreatorImpl(repository);GroupMember member = creator.create(groupId, name);// 3. 断言(Assertion)Assertions.assertThat(member).isNotNull();Assertions.assertThat(member.getGroupId()).isEqualTo(groupId);Assertions.assertThat(member.getName()).isEqualTo(name);}
}

从上面测试用例来看,测试的三个步骤:一是用mockito进行mock对象,指定mock对象执行方法的返回值(可根据参数返回);二是调业务逻辑代码进行测试;三是对测试结果进行断言。

通过测试可以反过来思考方法应该如何设计,其规则大概是看输入输出,也就是有什么输入就预期响应的输出,要注意的是方法返回值不一定是唯一的输出,如果方法内有数据库等操作,写入数据库的内容也算一个输出,这个时候可以考虑把数据库的输入反馈到方法返回值当中,总之是要验证指定的输入有指定的输出。

注:调save()保存数据严格来说也不算业务,其只与技术有关,可以进一步从业务逻辑中剥离出来。上面主要是方便说明mock一个对象的例子。

2.3 文档

2.3.1 Mockito

Mockito还是需要学习一下具体的用法的,重点可以先了解如何匹配参数,然后如何指定返回值(含抛异常)等,再高级的用法则可以用到再查找文档。

Mockito各个版本的文档列表:https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html

Mockito-4.5.1的详细文档:https://javadoc.io/doc/org.mockito/mockito-core/4.5.1/org/mockito/Mockito.html

2.3.2 AssertJ

AssertJ则通过方法名称就大概了解其用法了,重点可以了解一下在抛异常的场景如何进行断言。

文档:https://assertj.github.io/doc/

3 架构一小步

规范:把业务规则抽离出来,不依赖数据库和中间件进行详细的单元测试。

规范:编写可测试的代码,如果测试代码有逻辑则反向说明代码可测试性不足。

http://www.xdnf.cn/news/15350.html

相关文章:

  • 为什么有些PDF无法复制文字?原理分析与解决方案
  • 知识宇宙-思考篇:AI大模型如何重塑软件开发流程?
  • MCP选型指南:AWS vs Azure vs GCP vs 国内云厂商深度对比
  • openGauss 的列式存储表时遇到的排序和聚合查询性能问题
  • mybatis模糊匹配采用concat与#{},动态sql讲解
  • Flutter、React Native、Uni-App 的比较与分析
  • 80. 删除有序数组中的重复项 II
  • brpc中bthread_start_urgent和tls_task_group详细机制分析
  • 使用python 实现一个http server
  • 传感器WSNs TheDataLinkLayer——X-MAC
  • 基于随机森林的金融时间序列预测系统:从数据处理到实时预测的完整流水线
  • [特殊字符] 实时数据洪流突围战:Flink+Paimon实现毫秒级分析的架构革命(附压测报告)——日均百亿级数据处理成本降低60%的工业级方案
  • 【离线数仓项目】——电商域DWS层开发实战
  • 使用FastAdmin框架开发
  • 蒙特卡洛树搜索方法实践
  • 【云端深度学习训练与部署平台】AutoDL连接VSCode运行深度学习项目的全流程
  • C# 接口(派生成员作为实现)
  • 钉钉企业应用开发实战:从零构建组织级业务工具
  • MySQL 内外连接
  • monorepo 发布库 --- 打包文件
  • 线程属性设置全攻略
  • 深入详解:决策树在医学影像脑部疾病诊断中的应用与实现
  • 构建AI Agent的完整实战指南:从邮件助手案例看6步落地方法
  • 幸福的蓝图——搭建你的“快乐与意义”金字塔
  • AI 助力编程:Cursor Vibe Coding 场景实战演示
  • 分音塔科技(BABEL Technology) 的公司背景、股权构成、产品类型及技术能力的全方位解读
  • 部署Harbor私有仓库
  • 自动化证书续签工具针对VPS服务器HTTPS服务的维护实践
  • 2025 年 06 月 GitHub 十大热门项目排行榜
  • 【Docker基础】Dockerfile核心概念解析:什么是Dockerfile?与镜像、容器的关系