当前位置:网站首页>代码重构:面向单元测试
代码重构:面向单元测试
2022-08-03 23:22:00 【阿里云云栖号】
作 者 | 杜沁园(悬衡)
重构代码时,我们常常纠结于这样的问题:
需要进一步抽象吗?会不会导致过度设计?
如果需要进一步抽象的话,如何进行抽象呢?有什么通用的步骤或者法则吗?
不可测试的代码
代码不够简洁?
不好维护?
不符合个人习惯?
过度设计,不好理解?
“单测很容易书写,很容易就全覆盖了”,那么这就是可测试的代码;
“虽然能写得出来,但是费了老大劲,使用了各种框架和技巧,才覆盖完全”,那么这就是可测试性比较差的代码;
“完全不知道如何下手写”,那么这就是不可测试的代码;
public void producerConsumer() {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
Thread producerThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
blockingQueue.add(i + ThreadLocalRandom.current().nextInt(100));
}
});
Thread consumerThread = new Thread(() -> {
try {
while (true) {
Integer result = blockingQueue.take();
System.out.println(result);
}
} catch (InterruptedException ignore) {
}
});
producerThread.start();
consumerThread.start();
}
生产者:将 0-9 的每个数字,分别加上 [0,100) 的随机数后通过阻塞队列传递给消费者;
消费者:从阻塞队列中获取数字并打印;
需要测试的逻辑位于异步线程中,对于它什么时候执行?什么时候执行完?都是不可控的;
逻辑中含有随机数;
消费者直接将数据输出到标准输出中,在不同环境中无法确定这里的行为是什么,有可能是输出到了屏幕上,也可能是被重定向到了文件中;
可测试意味着什么?
<-50
f(-51) == -100
[-50, 50]
f(-25) == -50
f(25) == 50
>50
f(51) == 100
边界情况
f(-50) == -100
f(50) == 100
每一个分段其实就是代码中的一个条件分支,用例的分支覆盖率达到了 100%;
像 2x 这样的逻辑运算,通过几个合适的采样点就可以保证正确性;
边界条件的覆盖,就像是分段函数的转折点;
函数的返回值只和参数有关,只要参数确定,返回值就是唯一确定的
代码中含有远程调用,无法确定这次调用是否会成功;
含有随机数生成逻辑,导致行为不确定;
执行结果和当前日期有关,比如只有工作日的早上,闹钟才会响起;
public int f() {
return ThreadLocalRandom.current().nextInt(100) + 1;
}
public Supplier<Integer> g(Supplier<Integer> integerSupplier) {
return () -> integerSupplier.get() + 1;
}
public void testG() {
Supplier<Integer> result = g(() -> 1);
assert result.get() == 2;
}
public int g2(Supplier<Integer> integerSupplier) {
return integerSupplier.get() + 1;
}
因为这个例子比较简单,“可测试” 带来的收益看起来没有那么高,真实业务中的逻辑一般比 +1 要复杂多了,此时如果能构建有效的测试将是非常有益的。
面向单测的重构
第一轮重构
我们本章回到开头的生产者消费者的例子,用上一章学习到的知识对它进行重构。
public <T> void producerConsumerInner(Consumer<Consumer<T>> producer,
Consumer<Supplier<T>> consumer) {
BlockingQueue<T> blockingQueue = new LinkedBlockingQueue<>();
new Thread(() -> producer.accept(blockingQueue::add)).start();
new Thread(() -> consumer.accept(() -> {
try {
return blockingQueue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
})).start();
}
public <T> void producerConsumerInner(Executor executor,
Consumer<Consumer<T>> producer,
Consumer<Supplier<T>> consumer) {
BlockingQueue<T> blockingQueue = new LinkedBlockingQueue<>();
executor.execute(() -> producer.accept(blockingQueue::add));
executor.execute(() -> consumer.accept(() -> {
try {
return blockingQueue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}));
}
private void testProducerConsumerInner() {
producerConsumerInner(Runnable::run,
(Consumer<Consumer<Integer>>) producer -> {
producer.accept(1);
producer.accept(2);
},
consumer -> {
assert consumer.get() == 1;
assert consumer.get() == 2;
});
}
public abstract class ProducerConsumer<T> {
private final Executor executor;
private final BlockingQueue<T> blockingQueue;
public ProducerConsumer(Executor executor) {
this.executor = executor;
this.blockingQueue = new LinkedBlockingQueue<>();
}
public void start() {
executor.execute(this::produce);
executor.execute(this::consume);
}
abstract void produce();
abstract void consume();
protected void produceInner(T item) {
blockingQueue.add(item);
}
protected T consumeInner() {
try {
return blockingQueue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
private void testProducerConsumerAbCls() {
new ProducerConsumer<Integer>(Runnable::run) {
@Override
void produce() {
produceInner(1);
produceInner(2);
}
@Override
void consume() {
assert consumeInner() == 1;
assert consumeInner() == 2;
}
}.start();
}
很显然这种测试无法验证多线程运行的情况,但我故意这么做的,这部分单元测试的主要目的是验证逻辑的正确性,只有先验证逻辑上的正确性,再去测试并发才比较有意义,在逻辑存在问题的情况下就去测试并发,只会让问题隐藏得更深,难以排查。一般开源项目中会有专门的单元测试去测试并发,但是因为其编写代价比较大,运行时间比较长,数量会远少于逻辑测试。
public void producerConsumer() {
new ProducerConsumer<Integer>(Executors.newFixedThreadPool(2)) {
@Override
void produce() {
for (int i = 0; i < 10; i++) {
produceInner(i + ThreadLocalRandom.current().nextInt(100));
}
}
@Override
void consume() {
while (true) {
Integer result = consumeInner();
System.out.println(result);
}
}
}.start();
}
随机数生成逻辑
打印逻辑
public class NumberProducerConsumer extends ProducerConsumer<Integer> {
private final Supplier<Integer> numberGenerator;
private final Consumer<Integer> numberConsumer;
public NumberProducerConsumer(Executor executor,
Supplier<Integer> numberGenerator,
Consumer<Integer> numberConsumer) {
super(executor);
this.numberGenerator = numberGenerator;
this.numberConsumer = numberConsumer;
}
@Override
void produce() {
for (int i = 0; i < 10; i++) {
produceInner(i + numberGenerator.get());
}
}
@Override
void consume() {
while (true) {
Integer result = consumeInner();
numberConsumer.accept(result);
}
}
}
private void testProducerConsumerInner2() {
AtomicInteger expectI = new AtomicInteger();
producerConsumerInner2(Runnable::run, () -> 0, i -> {
assert i == expectI.getAndIncrement();
});
assert expectI.get() == 10;
}
public void producerConsumer() {
new NumberProducerConsumer(Executors.newFixedThreadPool(2),
() -> ThreadLocalRandom.current().nextInt(100),
System.out::println).start();
}
单元测试的边界
重构的工作流
过度设计
和 TDD 的区别
红灯:写用例,运行,无法通过用例
绿灯:用最快最脏的代码让测试通过
重构:将代码重构得更加优雅
代码结构尚未完全确定,出入口尚未明确,即使提前写了单元测试,后面大概率也要修改
产品一句话需求,外加对系统不够熟悉,用例很难在开发之前写好
业务实例 - 导出系统重构
启动一个线程,在内存中异步生成 Excel
上传 Excel 到钉盘/oss
发消息给用户
异步执行导致不可测试:抽出一个同步的函数;
大量使用 Spring Bean 导致逻辑割裂:将逻辑放到普通的 Java 类或者静态方法中;
表单数据,流程与用户的相关信息查询是远程调用,含有副作用:通过高阶函数将这些副作用抽出去;
导入状态落入数据库,也是一个副作用:同样通过高阶函数将其抽象出去;
public byte[] export(FormConfig config, DataService dataService, ExportStatusStore statusStore) {
//... 省略具体逻辑, 其中包括所有可测试的逻辑, 包括表单数据转换,excel 生成
}
config:数据,表单配置信息,含有哪些控件,以及控件的配置
dataService: 函数,用于批量分页查询表单数据的副作用
public interface DataService {
PageList<FormData> batchGet(String formId, Long cursor, int pageSize);
}
public interface ExportStatusStore {
/**
* 将状态切换为 RUNNING
*/
void runningStatus();
/**
* 将状态置为 finish
* @param fileId 文件 id
*/
void finishStatus(Long fileId);
/**
* 将状态置为 error
* @param errMsg 错误信息
*/
void errorStatus(String errMsg);
}
public void testExport() {
// 这里的 export 就是刚刚展示的导出测试边界
byte[] excelBytes = export(new FormConfig(), new LocalDataService(),
new LocalStatusStore());
assertExcelContent(excelBytes, Arrays.asList(
Arrays.asList("序号", "表格", "表格", "表格", "创建时间", "创建者"),
Arrays.asList("序号", "物品编号", "物品名称", "xxx", "创建时间", "创建者"),
Arrays.asList("1", "22", "火车", "而非", "2020-10-11 00:00:00", "悬衡")
));
}
通过 DataService 的抽象,系统可以支持多种数据源导出,比如来自搜索,或者来自 db 的,只要传入不同的 DataService 实现即可,完全不需要改动和性逻辑;
ExportStatusStore 的抽象,让系统有能力使用不同的状态存储,虽然目前使用的是 db,但是也可以在不改核心逻辑的情况下轻松切换成 tair 等其他中间件;
单元测试的局限性
一种可灰度的接口迁移方案
千万级可观测数据采集器 - iLogtail 代码完整开源
全链路压测:影子库与影子表之争
全链路灰度在数据库上我们是怎么做的?
企业上云|数字化转型经验分享
阿里云主长春:助力“专精特新”,数字科技陪伴企业成长
云钉低代码新模式、新能力、新机遇
推文科技:AI 解决方案助力内容出海
三星堆奇幻之旅:只有云计算才能带来的体验
不止能上路,更能做好服务:自动驾驶产品规模化的问题定义
自动驾驶,未来的移动智能载体?
如何提出关键问题
支撑10万人同时在线互动,是实现元宇宙的基本前提?
边栏推荐
- SolidEdge ST8安装教程
- 重发布实验报告
- 云平台建设解决方案
- 跨域的学习
- rosbridge-WSL2 && carla-win11
- Use tf.image.resize() and tf.image.resize_with_pad() to resize images
- rsync 基础用法
- Pytest学习-skip/skipif
- FinClip,助长智能电视更多想象空间
- Network basic learning series four (network layer, data link layer and some other important protocols or technologies)
猜你喜欢
Creo9.0 绘制中心线
Recognized by International Authorities | Yunzhuang Technology was selected in "RPA Global Market Pattern Report, Q3 2022"
走迷宫 BFS
重发布实验报告
智能座舱的「交互设计」大战
代码随想录笔记_动态规划_416分割等和子集
win10系统下yolov5-V6.1版本的tensorrt部署细节教程及bug修改
如何创建一个Web项目
End-to-End Lane Marker Detection via Row-wise Classification
逆波兰表达式求值
随机推荐
The sword refers to the offer question 22 - the Kth node from the bottom in the linked list
Interpretation of ML: A case of global interpretation/local interpretation of EBC model interpretability based on titanic titanic rescued binary prediction data set using interpret
libnet
【职场杂谈】售前与销售工作配合探讨
什么是memoization,它有什么用?
云平台建设解决方案
log4j-slf4j-impl cannot be present with log4j-to-slf4j
win10系统下yolov5-V6.1版本的tensorrt部署细节教程及bug修改
JS获得URL超链接的参数值
First domestic open source framework 】 【 general cloud computing framework, any program can be made into cloud computing.
Testng listener
Deep integration of OPC UA and IEC61499 (1)
ts用法大全
IELTS essay writing template
【LeetCode】最长公共子序列(动态规划)
MiniAPI of .NET6 (14): Cross-domain CORS (Part 1)
Creo9.0 绘制中心线
Creo 9.0创建几何点
栈的压入、弹出序列
Recognized by International Authorities | Yunzhuang Technology was selected in "RPA Global Market Pattern Report, Q3 2022"