设计模式之美
王争
前Google工程师,《数据结构与算法之美》专栏作者
立即订阅
18879 人已学习
课程目录
已更新 32 讲 / 共 100 讲
0/6登录后,你可以任选6讲全文学习。
开篇词 (1讲)
开篇词 | 一对一的设计与编码集训,让你告别没有成长的烂代码!
免费
设计模式学习导读 (3讲)
01 | 为什么说每个程序员都要尽早地学习并掌握设计模式相关知识?
02 | 从哪些维度评判代码质量的好坏?如何具备写出高质量代码的能力?
03 | 面向对象、设计原则、设计模式、编程规范、重构,这五者有何关系?
设计原则与思想:面向对象 (11讲)
04 | 理论一:当谈论面向对象的时候,我们到底在谈论什么?
05 | 理论二:封装、抽象、继承、多态分别可以解决哪些编程问题?
06 | 理论三:面向对象相比面向过程有哪些优势?面向过程真的过时了吗?
07 | 理论四:哪些代码设计看似是面向对象,实际是面向过程的?
08 | 理论五:接口vs抽象类的区别?如何用普通的类模拟抽象类和接口?
09 | 理论六:为什么基于接口而非实现编程?有必要为每个类都定义接口吗?
10 | 理论七:为何说要多用组合少用继承?如何决定该用组合还是继承?
11 | 实战一(上):业务开发常用的基于贫血模型的MVC架构违背OOP吗?
12 | 实战一(下):如何利用基于充血模型的DDD开发一个虚拟钱包系统?
13 | 实战二(上):如何对接口鉴权这样一个功能开发做面向对象分析?
14 | 实战二(下):如何利用面向对象设计和编程开发接口鉴权功能?
设计原则与思想:设计原则 (12讲)
15 | 理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?
16 | 理论二:如何做到“对扩展开放、修改关闭”?扩展和修改各指什么?
17 | 理论三:里式替换(LSP)跟多态有何区别?哪些代码违背了LSP?
18 | 理论四:接口隔离原则有哪三种应用?原则中的“接口”该如何理解?
19 | 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?
20 | 理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?
21 | 理论七:重复的代码就一定违背DRY吗?如何提高代码的复用性?
22 | 理论八:如何用迪米特法则(LOD)实现“高内聚、松耦合”?
23 | 实战一(上):针对业务系统的开发,如何做需求分析和设计?
24 | 实战一(下):如何实现一个遵从设计原则的积分兑换系统?
25 | 实战二(上):针对非业务的通用框架开发,如何做需求分析和设计?
26 | 实战二(下):如何实现一个支持各种统计规则的性能计数器?
设计原则与思想:规范与重构 (3讲)
27 | 理论一:什么情况下要重构?到底重构什么?又该如何重构?
28 | 理论二:为了保证重构不出错,有哪些非常能落地的技术手段?
29 | 理论三:什么是代码的可测试性?如何写出可测试性好的代码?
不定期加餐 (2讲)
加餐一 | 用一篇文章带你了解专栏中用到的所有Java语法
加餐二 | 设计模式、重构、编程规范等相关书籍推荐
设计模式之美
登录|注册

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

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

编写可测试代码案例实战

刚刚提到的这几个关于代码可测试性的问题,我准备通过一个实战案例来讲解。具体的被测试代码如下所示。
其中,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/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《设计模式之美》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(23)

  • 辣么大
    参考争哥今天的代码写了例子中的测试(可运行):
    https://github.com/gdhucoder/Algorithms4/tree/master/designpattern/u29

    今天学习到了高级的单元测试方法:
    1、依赖外部单例:将单例封装
    2、未决行为:例时间、随机数。将未决行为重新封装,测试时mock,使用匿名类。

     关于讨论1:需要mock的情况id会写入数据库的话,测试后需要恢复现场。曾经遇到过这么一个情况,id是通过一张表维护的,大于0,在代码中id的数据类型是Integer(遗留代码),由于测试时没有恢复现场,导致测试数据库中id增加过快,超过了代码中Integer的表示范围,而产生了意想不到的问题。
    2020-01-08
    7
  • 安静的boy
    这节满满的干货👍👍👍
    2020-01-08
    7
  • 失火的夏天
    思考题1,该方法逻辑就是填充一个ID,基本都是内部实现的一个id生成器,可以不用重写。一定要重写也行,自己弄一个自增id实现就行了。
    思考题2,提供方法的类不要new,也就是我们常说的service类,这个是要依赖注入的。提供属性的类,比如vo,bo,entity这些就可以new。
    2020-01-08
    5
  • 下雨天
    问题回答:
    1. IdGenerator.generateTransactionId()有未决行为逻辑,但不是说有未决行为就一定影响可测试性,前提是需要看未决行为是否有测试必要性,此处生成一个随机数(类似 System.currentTimeMillis()),测试意义不大!

    2.贫血模型实体类
    2020-01-08
    2
  • QQ怪
    看到一半,我就来评论,老师收下我的膝盖,太强了

    作者回复: 😁 感谢认可!

    2020-01-08
    2
  • 桂城老托尼
    感谢争哥分享
    课后讨论1.id的生成逻辑有点没看懂,单纯从代码覆盖上看,fillTransactionId 未覆盖完全,需要mock下这个静态方法,当然也有其他分支逻辑可以覆盖。
    id没有在execute方法中不是核心属性(mock方法的入参),不影响execute的可测试性。 id的生成用静态方法真的好么?
    2.有行为的对象不适合在类中new,尽量使用依赖注入,依赖接口编程,而不是具体的实现。 数据对象适合在类中new 比如各种model do vo info 。
    一家之言欢迎讨论指正。
    2020-01-08
    2
  • 逍遥思
    1. 不会影响可测试性,因为 generateTransactionId 并不需要依赖什么外部服务,所以也不需要 mock
    2. 不是。不依赖外部服务的类就可以内部创建,比如 String
    2020-01-08
    1
  • Jesse
    思考题1,该方法产生一个唯一的ID,我认为不需要mock。
    思考题2,我觉得如果对象有行为,并且行为与外部系统交互或者执行的结果具有不确定性,就需要依赖注入来完成测试。如果对象的行为是可预测的并且唯一的,可以直接new。
    2020-01-08
    1
  • #HEAVEN
    你好,没有找到作者邮箱,想问一个问题;
    作者在开发一个需求的时候是怎样的一个流程,设计做到那种程度?
    比如说一般我会做1. 需求分析,列出哪些需求case; 2. 列出这些case需要开发哪些功能点;3. 主要涉及到哪些类,结构如何组织;4. 主要类的主要职责等;5. 开始code了;
    在开发的过程中也会遇到一些问题,比如,有时候有些类的职责或者结构开始的设计不太合理,需要一些修改;这个时候我就在怀疑,是不是前期做的设计不够充分造成的。也看到一些书上会把类的属性、方法都设计出来,还有主要流程case的序列图;但是这样做耗时较多,很多时候项目日程不允许。
    像问一下,作者在开发中设计阶段有哪些流程,做到什么程度?
    留个邮箱方便交流就更好了
    2020-01-08
  • 荀麒睿
    对于IdGenerator.generateTransactionId(),虽然是未决行为,个人认为只是生成一个id了话,并不会包含非常复杂的逻辑操作,应该就跟Math.abs()类似,不需要进行mock
    2020-01-08
  • 再见孙悟空
    今天老师讲的为了更好的单元测试而进行的重构,原来工作中无形间已经用到了。在对接三方 api 时,有时候缺少必要的参数信息,我们只能模拟调通,这时候我们就写一个类继承原始类,重写原方法,返回自己需要的数据,不过还有很多做的不足,例如对于不确定数据的mock 没有抽成方法等,持续学习,老师棒!
    2020-01-08
  • 斐波那契
    感觉那个createtimestamp那边 如果没有set方法应该可以用反射去修改这个属性

    思考题1 可以不mock 因为执行idgenerator之前有逻辑判断的 只要传入进去的参数不满足条件就不会走 其次对于id开头添加t_这个逻辑跟id生成器没有关系 只要保证造出来到id没有t_开头就可以测试

    思考题2 其实最近在写一个需求 我就用了内部类 也觉得并没有破坏测试性 我这个内部类主要是为了隐藏某个接口的实现 不想被调用者在使用外部类时滥用我的每一个接口实现方法 起到一个保护作用 对于测试性 完全可以通过不同的外部类参数来进行调整 其实对于内部类的可测试性来讲 只要外部类有足够的参数来控制内部类就可以 对于内部类调用第三方的情况 只要外部类有参数可以注入就可以用mock来修改内部类的实现
    2020-01-08
  • Jxin
    1.栏主好像提过,要谈谈分层对于可测试性的影响,不知是不是我记错了,这篇没提到哈。

    回答问题
    1.交易id这东西,是全局唯一的。不该被mock,mock了不仅没用,反而可能会有其他问题(如果有引入唯一键检验相关机制的话,比如幂等啥的)。

    2.值对象可以new,因为值对象不会有涉及改动自身属性的方法,也就是说它通常是不可变的,所以也没什么检验的意义。而实体领域模型不一定可以new,因为其方法会改变自身属性,而对这些属性变动,有时候我们需要校验。而贫血实体dto或do之类的,一般也可以new,因为它只承接属性,场景类似值对象,只需要关心方法返回的dto或vo的值即可,无需关心方法内部是new还是注入的(对于方法而言,除了类成员属性的注入,方法入参也算注入吧)。
    2020-01-08
  • 平风造雨
    // 抽取了当前时间获取的逻辑,方便测试
        private long currentTimeMillis;
        private Date dueTime;
        public Demo(Date dueTime){
            this.dueTime = dueTime;
            this.currentTimeMillis = getCurrentTimeMillis();
        }

        protected long getCurrentTimeMillis(){
            return System.currentTimeMillis();
        }
        public long caculateDelayDays() {
            if(dueTime.getTime() >= currentTimeMillis){
                return 0;
            }
            long delayTime = currentTimeMillis - dueTime.getTime();
            long delayDays = delayTime / 86400_000;
            return delayDays;
        }
        @Test
        public void testCaculateDelayDays(){
            TimeZone timeZone = TimeZone.getTimeZone("Asia/ShangHai");
            Calendar calendar = Calendar.getInstance(timeZone);
            calendar.clear();
            calendar.set(2020, Calendar.FEBRUARY,1,0,0,0);
            Date dueTime = calendar.getTime();
            Demo demo = new DemoClassOne(dueTime);
            Assert.assertEquals(demo.caculateDelayDays(), 0);
            calendar.clear();
            calendar.set(2019, Calendar.DECEMBER, 31, 0,0,0);
            dueTime = calendar.getTime();
            demo = new DemoClassOne(dueTime);
            Assert.assertEquals(demo.caculateDelayDays(), 1);
        }

        public static class DemoClassOne extends Demo {
            public DemoClassOne(Date dueTime) {
                super(dueTime);
            }
            @Override
            protected long getCurrentTimeMillis() {
                TimeZone timeZone = TimeZone.getTimeZone("Asia/ShangHai");
                Calendar calendar = Calendar.getInstance(timeZone);
                calendar.clear();
                calendar.set(2020, Calendar.JANUARY,1,0,0,0);
                return calendar.getTimeInMillis();
            }
        }
    2020-01-08
  • 李小四
    设计模式_29:

    1. 我认为静态方法```IdGenerator.generateTransactionId()```不需要mock,因为它不会很耗时(如果实现比较正常),也没有未决行为,除非对于id有特殊的要求,否则不需要mock。

    2. 这道题我想不清楚,想看看王争老师和大家的看法。
    2020-01-08
  • 此鱼不得水
    1. 未决行为 中提到的单测,可以把不确定的变量‘当前时间’提取出来作为入参
    2020-01-08
  • 守拙
    课堂讨论:



    Q1.实战案例中的 void fillTransactionId(String preAssignedId) 函数中包含一处静态函数调用:IdGenerator.generateTransactionId(),这是否会影响到代码的可测试性?在写单元测试的时候,我们是否需要 mock 这个函数?



    Answer1:

    理论上讲fill()方法由于内部静态方法的使用,及id生成的未决行为,影响可测试性.

    解决方法是为fill()方法添加一个形参,generateId,如下:

    void fillTransactionId(@Nullable String preAssignedId, @Nullable String generateId)

    但这样做会影响封装性.fill()方法内部逻辑简单,对可测试性的影响是微不足道的.除非测试问题直指fill()方法,否则个人倾向于不做修改.



    Q2.我们今天讲到,依赖注入是提高代码可测试性的最有效的手段。所以,依赖注入,就是不要在类内部通过 new 的方式创建对象,而是要通过外部创建好之后传递给类使用。那是不是所有的对象都不能在类内部创建呢?哪种类型的对象可以在类内部创建并且不影响代码的可测试性?你能举几个例子吗?



    Answer2: 内部类或静态内部类, 局部类的对象可以在类内部通过new 的方式初始化.它们是外部类行为的一部分,仅为外部类自己使用,不影响测试性.



    对于未决行为方法的改造:

    before:



    public class Demo {

    public long caculateDelayDays(Date dueTime){

     long currentTimestamp = System.currentTimeMillis();

     if (dueTime.getTime() >= currentTimestamp) { return 0; }

    long delayTime = currentTimestamp - dueTime.getTime();

     long delayDays = delayTime / 86400;

    return delayDays;

    ​ }

    }



    after:

    public class Demo {

    public long caculateDelayDays(Date dueTime, Date currentTime){

    ​ long currentTimestamp = currentTime.getTime();

    ​ if (dueTime.getTime() >= currentTimestamp) { return 0; }

    ​ long delayTime = currentTimestamp - dueTime.getTime();

    ​ long delayDays = delayTime / 86400;

    ​ return delayDays;

    ​ }

    }
    2020-01-08
    1
  • Jackey
    ID生成方法只是生成一个字符串,个人认为可以不用测试
    2020-01-08
  • 美美
    有多个通过spring注入的类时,应该怎么做测试呢?

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

    2020-01-08
    2
  • liu_liu
    1. 生成的id 并不会对主体逻辑造成影响,无需mock,只需该 idGen 本身通过单元测试即可。
    2. 如果未使用到依赖对象的功能,或者其功能很简单,又或者使用了也不会影响到当前被测函数逻辑,可以内部 new。
    2020-01-08
收起评论
23
返回
顶部