设计模式之美
王争
前 Google 工程师,《数据结构与算法之美》专栏作者
123425 人已学习
新⼈⾸单¥98
登录后,你可以任选6讲全文学习
课程目录
已完结/共 113 讲
设计模式与范式:行为型 (18讲)
设计模式之美
15
15
1.0x
00:00/00:00
登录|注册

29 | 理论三:什么是代码的可测试性?如何写出可测试性好的代码?

探讨了依赖注入的有效性,以及哪种类型的对象可以在类内部创建并且不影响代码的可测试性
讨论了实战案例中的void fillTransactionId(String preAssignedId)函数中包含的静态函数调用对代码可测试性的影响
重构高耦合的代码
重构复杂继承关系
重构代码中的静态方法
重构代码中的全局变量
重构代码中的未决行为逻辑
介绍了mock的方式,手动mock和利用框架mock
重构代码以提高可测试性
通过实战案例讲解了如何利用依赖注入来提高代码的可测试性
高度耦合的代码
使用复杂的继承关系
滥用静态方法
滥用可变全局变量
未决行为逻辑
通过依赖注入,可以通过mock的方法解依赖外部服务
依赖注入是编写可测试性代码的最有效手段
如果很难为代码编写单元测试,意味着代码设计不够合理,可测试性不好
代码的可测试性是指针对代码编写单元测试的难易程度
课堂讨论
实战案例
常见的Anti-Patterns
编写可测试性代码的最有效手段
什么是代码的可测试性?
如何写出可测试性好的代码?

该思维导图由 AI 生成,仅供参考

在上一节课中,我们对单元测试做了介绍,讲了“什么是单元测试?为什么要编写单元测试?如何编写单元测试?实践中单元测试为什么难贯彻执行?”这样几个问题。
实际上,写单元测试并不难,也不需要太多技巧,相反,写出可测试的代码反倒是件非常有挑战的事情。所以,今天,我们就再来聊一聊代码的可测试性,主要包括这样几个问题:
什么是代码的可测试性?
如何写出可测试的代码?
有哪些常见的不好测试的代码?
话不多说,让我们正式开始今天的学习吧!

编写可测试代码案例实战

刚刚提到的这几个关于代码可测试性的问题,我准备通过一个实战案例来讲解。具体的被测试代码如下所示。
其中,Transaction 是经过我抽象简化之后的一个电商系统的交易类,用来记录每笔订单交易的情况。Transaction 类中的 execute() 函数负责执行转账操作,将钱从买家的钱包转到卖家的钱包中。真正的转账操作是通过调用 WalletRpcService RPC 服务来完成的。除此之外,代码中还涉及一个分布式锁 DistributedLock 单例类,用来避免 Transaction 并发执行,导致用户的钱被重复转出。
public class Transaction {
private String id;
private Long buyerId;
private Long sellerId;
private Long productId;
private String orderId;
private Long createTimestamp;
private Double amount;
private STATUS status;
private String walletTransactionId;
// ...get() methods...
public Transaction(String preAssignedId, Long buyerId, Long sellerId, Long productId, String orderId) {
if (preAssignedId != null && !preAssignedId.isEmpty()) {
this.id = preAssignedId;
} else {
this.id = IdGenerator.generateTransactionId();
}
if (!this.id.startWith("t_")) {
this.id = "t_" + preAssignedId;
}
this.buyerId = buyerId;
this.sellerId = sellerId;
this.productId = productId;
this.orderId = orderId;
this.status = STATUS.TO_BE_EXECUTD;
this.createTimestamp = System.currentTimestamp();
}
public boolean execute() throws InvalidTransactionException {
if ((buyerId == null || (sellerId == null || amount < 0.0) {
throw new InvalidTransactionException(...);
}
if (status == STATUS.EXECUTED) return true;
boolean isLocked = false;
try {
isLocked = RedisDistributedLock.getSingletonIntance().lockTransction(id);
if (!isLocked) {
return false; // 锁定未成功,返回false,job兜底执行
}
if (status == STATUS.EXECUTED) return true; // double check
long executionInvokedTimestamp = System.currentTimestamp();
if (executionInvokedTimestamp - createdTimestap > 14days) {
this.status = STATUS.EXPIRED;
return false;
}
WalletRpcService walletRpcService = new WalletRpcService();
String walletTransactionId = walletRpcService.moveMoney(id, buyerId, sellerId, amount);
if (walletTransactionId != null) {
this.walletTransactionId = walletTransactionId;
this.status = STATUS.EXECUTED;
return true;
} else {
this.status = STATUS.FAILED;
return false;
}
} finally {
if (isLocked) {
RedisDistributedLock.getSingletonIntance().unlockTransction(id);
}
}
}
}
对比上一节课中的 Text 类的代码,这段代码要复杂很多。如果让你给这段代码编写单元测试,你会如何来写呢?你可以先试着思考一下,然后再来看我下面的分析。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了提高代码可测试性的关键方法,重点讨论了在编写可测试代码时可能遇到的挑战以及解决方法。通过实际案例展示了一个复杂的交易类代码,并提出了针对该类的六个测试用例。文章指出了在执行这些测试用例时可能遇到的问题,包括对外部服务的依赖、网络通信的耗时和不可控因素等。作者通过详细的技术讲解,展示了如何使用依赖注入和mock技术来提高代码的可测试性,使得单元测试不再依赖外部系统,从而更加可靠和高效。此外,文章还强调了良好的代码设计原则和思想对于代码可测试性的重要性,以及在平时的开发中要多思考代码编写是否容易进行单元测试,有利于设计出高质量的代码。总之,本文通过清晰的案例和详细的技术讲解,帮助读者了解了如何应对代码可测试性的挑战,对于软件开发人员具有一定的参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《设计模式之美》
新⼈⾸单¥98
立即购买
登录 后留言

全部留言(116)

  • 最新
  • 精选
  • 楊_宵夜
    争歌, 代码中isExpired()方法的修饰符是protected, 如果某些方法从设计原则来说应该设置为private的话, 那么这样的手动mock的方式是否就不适用了呢? 换个角度来提问: 为了维持可测试性, 在代码中加入过多protected的方法, 是否合理呢?

    作者回复: 也是没办法的事情,理论上应该是private的。所以会有@VisibleForTesting这样的annotation

    2020-01-20
    9
    76
  • 张三丰
    老师,下面这句话不是很理解,如果我的某个接口就是需要依赖很多服务才能把结果正确返回给前端,这时候怎么办?比如查询购物车,需要访问商品服务的商品信息,优惠服务的优惠信息,同时访问价格服务的价格信息等等,这个时候,高度耦合怎么去避免呢? 如果一个类职责很重,需要依赖十几个外部对象才能完成工作,代码高度耦合,那我们在编写单元测试的时候,可能需要 mock 这十几个依赖的对象。不管是从代码设计的角度来说,还是从编写单元测试的角度来说,这都是不合理的。

    作者回复: 这个不叫高度耦合吧。不是说耦合很多就是高度耦合,也要看业务需求啊,确实要这么多数据,那必然要以来这么多服务。还有,为了前端获取数据简单,可以用facade模式,包裹一层接口。

    2020-05-28
    21
  • QQ怪
    看到一半,我就来评论,老师收下我的膝盖,太强了

    作者回复: 😁 感谢认可!

    2020-01-08
    2
    17
  • Vincent.X
    手机看代码有老是要拖动,有什么解决的办法吗??

    作者回复: 只能@一下编辑了

    2020-06-17
    5
  • 美美
    有多个通过spring注入的类时,应该怎么做测试呢?

    作者回复: 可以借助springtest测试框架来做

    2020-01-08
    4
    4
  • Mew151
    有一个问题,如果测试方法A()中调用了本类的私有方法B(),这个时候该怎么处理呢?

    作者回复: 你指的处理什么呢?

    2020-08-26
    1
  • J.Smile
    想到一个问题,代码结构扁平化的极端结果可能会造成依赖对象过多吗?这种情况mock不是依然难搞吗

    作者回复: ”代码结构扁平化的极端结果“能举个例子吗?

    2020-01-08
    2
    1
  • qpzm7903
    请问贫血模式的mvc中的service怎么进行单元测试呢

    作者回复: 单元测试跟贫不贫血没关系吧

    2020-04-26
  • 失火的夏天
    思考题1,该方法逻辑就是填充一个ID,基本都是内部实现的一个id生成器,可以不用重写。一定要重写也行,自己弄一个自增id实现就行了。 思考题2,提供方法的类不要new,也就是我们常说的service类,这个是要依赖注入的。提供属性的类,比如vo,bo,entity这些就可以new。
    2020-01-08
    2
    178
  • 安静的boy
    这节满满的干货👍👍👍
    2020-01-08
    61
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部