乐趣区

关于java:从头到脚说单测谈有效的单元测试下篇

导读
在《从头到脚说单测——谈无效的单元测试(上篇)》中次要介绍了:金字塔模型、为何要做单测、单测的阶段及指标,在下篇中咱们次要介绍对于 mock、和如何不要滥用 mock、用例编写的策略等更多精彩内容,让咱们连忙来看一看吧~

七. 必须说一说 mock 了
test doubles
在《xUnit Test Patterns》一书中,作者首次提出 test doubles(测试替身)的概念。咱们常挂在嘴边的 mock 只是其中一种,而且是最容易与 Stub(打桩)混同的一种。在上一节中对 gomonkey 的介绍,你能够留神到了,我没有应用 mock,全副是 Stub。是的,gomonkey 不是 mock 工具,只是一个高级打桩的工具,适配了咱们大部分的应用场景。
测试替身,共有五种:
·Dummy Object
用于传递给调用者然而永远不会被实在应用的对象,通常它们只是用来填满参数列表
·Test Stub
Stubs 通常用于在测试中提供封装好的响应,譬如有时候编程设定的并不会对所有的调用都进行响应。Stubs 也会记录下调用的记录,譬如一个 email gateway 就是一个很好的例子,它能够用来记录所有发送的信息或者它发送的信息的数目。简而言之,Stubs 个别是对一个实在对象的封装
·Test Spy
Test Spy 像一个特务,安插在了 SUT 外部,专门负责将 SUT 外部的间接输入(indirect outputs) 传到内部。它的特点是将外部的间接输入返回给测试案例,由测试案例进行验证,Test Spy 只负责获取外部情报,并把情报收回去,不负责验证情报的正确性

·Mock Object
针对设定好的调用办法与须要响应的参数封装出适合的对象
·Fake Object
Fake 对象经常与类的实现一起起作用,然而只是为了让其余程序可能失常运行,譬如内存数据库就是一个很好的例子。
stub 与 mock
打桩和 mock 应该是最容易混同的,而且习惯上咱们对立用 mock 去形容模仿返回的能力,习惯成自然,也就把 mock 常挂在嘴边了。
就我的了解,stub 能够了解为 mock 的子集,mock 更弱小一些:

· mock 能够验证实现过程,验证某个函数是否被执行,被执行几次
· mock 能够依条件失效,比方传入特定参数,才会使 mock 成果失效
· mock 能够指定返回后果
· 当 mock 指定任何参数都返回固定的后果时,它等于 stub
只不过,go 的 mock 工具 gomock 只基于接口失效,不适宜新闻、企鹅号我的项目,而 gomonkey 的 stub 笼罩了大部分的应用场景。

八. 不要滥用 mock
我把这一部分独自放一章节,体现出它重要的意义。须要读懂肖鹏的《mock 七宗罪》,在 gitchat 上。

两个门派
约从 2004-2005 年间,江湖上造成两大门派:经典测试驱动开发派 和 mockist(mock 极端派)。

先说 mockist。他主张将被测函数所有调用的里面函数,全副 mock。也即,只关注被测函数本人的一行行代码,只有调用其余函数,全都 mock 掉,用假数据来测试。

再说经典测试驱动开发派,他们主张不要滥用 mock,能不 mock 就不 mock,被测单元也不肯定是具体的一个函数,可能是多个函数,串起来。必要的时候再 mock。

两个门派相争多年,实践各有利弊,至今依然共存。存在即正当。比方 mockist,应用了过多的 mock,无奈笼罩函数接口,这部分又是很容易出错的;经典派,串的太多,又被质疑是集成测试。

对于咱们理论利用,不用强制听从某一派,联合即可,须要的时候 mock,尽量少 mock,不必纠结。

什么时候适宜 mock

如果一个对象具备以下特色,比拟适宜应用 mock 对象:
· 该对象提供非确定的后果(比方以后的工夫或者以后的温度)
· 对象的某些状态难以创立或者重现(比方网络谬误或者文件读写谬误)
· 对象办法上的执行太慢(比方在测试开始之前初始化数据库)
· 该对象还不存在或者其行为可能发生变化(比方测试驱动开发中驱动创立新的类)
· 该对象必须蕴含一些专门为测试筹备的数据或者办法(后者不适用于动态类型的语言,风行的 Mock 框架不能为对象增加新的办法。Stub 是能够的。)
因而,不要滥用 mock(stub),当被测办法中调用其余办法函数,第一反馈应该走进去串起来,而不是从根部就 mock 掉了。
九. 用例设计法
看了一篇文章:像机器一样思考

文章讲述思考程序设计的基本思路——思考输入输出。咱们设计 case,想要失去最全面的设计,基本是思考全输出全输入的组合,当然,一方面,这么做耗时太大,很多时候是不可执行的;一方面,这不是想要的后果,要思考投入产出比。这时,须要实践与实际相结合,理论指导实际,实际精密实践。

先说实践

  1. 还是从上篇文章说起,思考输出、输入,就要先晓得哪些属于输入输出:
  2. 白盒 & 黑盒设计
    白盒法:
    ·逻辑笼罩(语句、分支、条件、条件组合等)
    ·门路(全门路、最小线性无关门路)
    ·循环:联合 5 种场景(跳过循环、循环一次,循环最大次,循环 m 次命中、循环 m 次未命中)

黑盒法:
等价类:正确的,谬误的(非法的,非法的)
边界法:[1,10] ==> 0,1,2,9,10,11(是等价类的无效补充)



  1. 联合利用
    全输入输出,施行难度较大,转而咱们思考到业内大神们设计出白盒黑盒设计法,通过认真思考,能够判断出是对全输出全输入的方法论体现。

因而,白盒 & 黑盒用例设计法,每一种我都亲自实际,了解其优缺点,从设计笼罩角度,条件组合 > 最小线性无关门路 > 条件 > 分支 > 语句。

上面这张图,是我晚期思考用例设计时的一次实际,当初回顾起来,它适度设计了。

但理论中,咱们放心“适度设计”,也还无奈给出答案“用什么办法设计保障十拿九稳”。
·适度设计,也会使 case 软弱
·在无限的工夫内,咱们寻求收益较大化

  1. 小函数 & 重要(计算,对象解决):尽量设计全面
  2. 逻辑较重,代码行数较多:分支、语句笼罩 + 循环 + 典型的边界解决(咱们看个例子:GetUserGiftList)
  3. 引出“基于实现”与“基于用意”的设计:过多去 Stub 被测函数外部的调用,就越靠近“基于实现”(第二次提到“基于用意”)

十. 基于用意与基于实现
这个话题是十分重要的。
基于用意:思考函数最终想做什么,把被测函数当做黑盒,思考其输入输入,而不要关注其中间是怎么实现的,到底生成了什么长期变量,循环了几次,有什么判断等。
基于实现:输入输出我也思考,两头怎么实现的我也思考。mock 就是一个好例子,比方咱们写一个 case,咱们会用 mock 去验证函数内是否调用了哪个内部办法、调用了几次,语句的执行程序是怎么的。程序的变动比需要还快,重构随时都有,稍有一变,case 大批量失败,这也是《mock 七宗罪》中提到的一种状况。
咱们要的是基于用意,远离基于实现。
联合实战经验,我总结如下:

  1. “要么写好,要么不写”。case 也是代码,也须要保护,也有工作量,所以要写的到位,而不是写得多。写了一堆没用的,你还得保护,不如删了。
  2. 拿到一个函数,先问问本人,这个函数要实现什么性能,最终输入是什么;而后,问本人,这个函数的危险在哪里,哪局部逻辑不太自信,最容易出错(计算、简单的判断、某异样分支的命中等)。这些才是咱们 case 要笼罩的点。
  3. 内联函数、间接 get/set,没几行没什么逻辑的,只有你判断没什么危险,就不必写 case。
  4. 确定了要写的 case,再用分支条件组合、边界等外围方面设计出具体用例,施行编写。
    能够联合新闻几次单测 case review 记录,来具体了解。
    咱们看一个具体的 case:
  5. 拿到这个函数,作为测试同学的我先向开发理解该函数的用意:对合乎格局、合乎工夫的用户礼物进行加和
    2. 读代码,理解了代码流程、几个异样分支,先做了 code review
    3. 依据必要的异样分支,设计 case 笼罩
  6. 对失常的业务流程,是依照开发讲述的函数用意,进行设计,case 如下:
    被测函数

    失常门路的单测 case

func TestNum_CorrectRet(t *testing.T) {
giftRecord := map[string]string{
“1:1000”: “10”,
“1:2001”: “100”,
“1:999”:  “20”,
“2”:      “200”,
“a”:      “30”,
“2:1001”: “20”,
“2:999”:  “200”,
}
 
expectRet := map[int]int{
1: 110,
2: 20,
}
 
var s *redis.xxx
patches := gomonkey.ApplyMethod(reflect.TypeOf(s), “Getxxx”, func(_ *redis.xxx, _ string)(map[string]string, error) {
return giftRecord, nil
})
defer patches.Reset()
 
p := &StarData{xxx}
userStarNum, err := p.GetNum(10000)
 
assert.Nil(t, err)
assert.JSONEq(t, Calorie.StructToString(expectRet), Calorie.StructToString(userStarNum))
 
}
有同学会问到:然而你最终还是看的代码呀?看到代码的正确逻辑是怎么解决的,再去设计的 case 和结构数据吧?而且你不看代码,怎么晓得有哪些异样分支要笼罩呢?

答:1. 我当初作为测试同学写开发同学的 case,的确须要晓得有哪些异样分支要解决,但不局限于代码中的几种,还应该包含我了解到的异样分支,都要体现在 case 中。咱们的 case 绝不是为了证实代码是怎么实现的!通过单测,咱们常常可能发现 bug。然而未来是开发来写单测的,他本人设计的函数必定晓得要笼罩哪些异样分支。

  1. 嗯,我须要看代码的失常流程是怎么的,但不代表着把代码扒下来以设计出 case。case 实际上是通过与开发的沟通后,理解输出数据的构造,输入的格局,数据校验和计算的过程,去设计输入输出的。

十一. 用例编写的策略

对于怎么个程序去写单测,咱们重点实际了一番,基本上也就三种状况吧:
·独立原子:mockist,被咱们颠覆了。当然,最底部的函数可能没有内部依赖,那单测它就够了。
·自上而下(红线):从入口函数往下测。实际的过程中,我发现很难执行,因为我从入口处就要想好每一次调用都须要返回哪些数据及格局,串起来一个 case 曾经十分不易。
·自下而上(黄线):咱们发现,入口函数,往往没什么逻辑,调用另一个函数而后拿到响应返回。所以入口函数,兴许不必写?咱们持续往下看,每一次调用的函数都看,也调出了以往的线上线下 bug,咱们发现呈现问题的代码局部往往是调用链的底端,尤其是波及计算、简单分支循环等。而且,底端的函数往往可测性较好。

因而,思考两方面,咱们抉择自下而上设计来选择函数编写 case:
1. 底部的函数可测性通常很好

  1. 外围逻辑比拟多,尤其波及计算、拼接,分支的。

十二. 可测性问题的解决——重构
导致无奈写单测的重要起因是,代码可测性不好。如果一个函数八九十行、二三百行,根本就是不可测的,或者说“不好测的”。因为外面逻辑太多了,从第一行到最初一行都经验了什么,各种函数调用内部依赖,各种 if/for,各种异样分支解决,写一个 case 的代码行数可能是原函数的几倍。
因而,推动单测走上来,重构晋升可测性是必须环节。而且,通过重构,代码构造间接清晰了,更可读可保护,更容易发现和定位问题。
常见的问题:反复代码、魔法数字、箭头式的代码等
举荐的实践书籍是《重构:改善既有代码的设计》第二版、《clean code》
我输入了一篇对于重构的文章。
应用 codecc(腾讯代码查看核心)的圈复杂度、函数长度来评估代码构造品质,咱们与开发一起学习,一起实际,一直有成绩输入。
对于箭头式的代码,可思考如下步骤:
1. 多应用卫语句,先判断异样,异样 return
2. 将判断语句抽离
3. 将外围局部抽离为函数

十三. 用例保护,可读性、可维护性、可信赖性
用例设计因素
·将外部逻辑与内部申请离开测试
·对服务边界(interface)的输出和输入进行严格验证
·用断言来代替原生的报错函数
·防止随机后果
·尽量避免断言工夫的后果
·适时应用 setup 和 teardown
·测试用例之间互相隔离,不要相互影响
·原子性,所有的测试只有两种后果:胜利和失败
·防止测试中的逻辑,即不该蕴含 if、switch、for、while 等
·不要爱护起来,try…catch…
·每个用例只测试一个关注点
·少用 sleep,延缓测试时长的行为都是不衰弱的
·3A 策略:arrange,action,assert
用例可读性
· 题目要明确表明用意,如 Test+ 被测函数名 +condition+result。case 失败后,通过名字就晓得哪个场景失败,而不必一行行再读代码。未来保护这个测试代码的,可能是其他人,咱们须要让他人容易读懂
·测试代码的内容要清晰,3A 准则:arrange,action,assert 分成三局部。数据筹备局部 arrange 如果代码行较多,思考抽离进来。
·断言的用意显著,能够思考将魔法数字变为变量,命名艰深易通
·一个 case,不要做过多的 assert,要专一
·和业务代码的要求统一,都要可读
用例可维护性
·反复:文本字符串反复、构造反复、语义反复
·回绝硬编码
·基于用意的设计。不要因为业务代码重构一次,就导致一批 case 失败
·留神代码的各种坏滋味,可参见《重构》第二版
用例可信赖性
单元测试,小而且运行快,它不是为了发现本次的 bug,更是为了放在流水线上 致力发现每一次 MR 是否产生了 bug。单测运行失败,惟一的起因只应该是呈现 bug,而不是因为内部依赖不稳固、基于实现的波及等,长期的失败将失去单元测试的警示作用,“狼来了”的故事是惨痛的教训。
·非被测程序缺点,随机失败的 case
·永不失败的 case
·没有 assert 的 case
·徒有虚名的 case
十四. 新闻单元测试的推动过程
咱们提到,对单元测试的实际分为 4 个阶段,每阶段均有指标。
第一阶段  会写,全员写,不要求写好
·由上而下的推动,从总监到组长,竭力反对,毫无犹豫,使组员情绪高涨
·疾速确定单测框架,纯熟应用
·联合开发需要,输入各场景下 单测框架的应用办法,包含 assert、mock,table-driven 等
·封装 http2WebContext,不便生成 context 对象
·屡次培训,解说单测实践及框架应用
·各团队(终端、接入层)指定单测接口人,由他先尝螃蟹。他是最相熟框架应用,在后期写最多 case 的人
·在磨合好单测框架的集成应用后,启动会,局部同学先试点应用,确保间断两个迭代,这几个同学都有 case 输入
·每个迭代总结数据中,退出单测相干数据:组长和总监十分关注单测数据信息,针对性激励晋升 case 数量和代码行数

第二阶段 写好,无效,全员写
· 测试同学摸索出 mock 的正确应用办法、用例设计的正确思路,分享给团队,通过探讨达成统一
· 结对编程,每迭代结对 2 - 3 个开发,独特写 case,相互晋升。
这里的结对是灵便的:有的开发,只需用半天的工夫给他讲框架应用,同他练习,他就能够上手了不须要再放心;有的开发,会分给测试同学需要,测试同学写完 case 后,开发 review 学习,并尝试写出本人的第一个 case;有的开发,一开始可能不太承受,以需要不适宜单测为理由,察看了一段时间,他发现其他人都写了,也没那么难,对团队也无利,他甚至会被动找到测试同学教他写 case。
·测试同学对开发提交的 case 进行 review,跟进开发批改后从新 MR
·间断两个迭代,邀请 dot 老师、乔帮主进行 case review,成果十分好
·对迭代的单测数据分析,关注需要覆盖度、人员覆盖度,case 增量
·组长继续激励反对单测
·每迭代的需要减少“单元测试”字段,由组长评估后置位。不带单测的 MR 不予通过,单测也要被 review

第三阶段 可测性晋升
·测试和开发独特学习《重构》第二版,每周有分享会
·某些骨干同学优先重构本人的代码
·测试同学严格要求,先保障有单测,而后小步重构,每一步均有单测保障
·通过流水线的 codecc 扫描,圈复杂度和函数长度必须达标,不可人工干预其通过
第四阶段 TDD
·先不保障开发同学做到 TDD,门槛还是挺高的,而且须要在线下纯熟之后再使用到业务开发中
·逐渐推动开发将业务代码和测试代码同步编写,而不是实现业务代码后再补 case
·测试同学练成 TDD
十五. 流水线

单测要放在流水线上跑,客户端和后盾都配好了流水线,保障每次 push 和 MR 都运行一次,发报告。
对于 go 的单测,新闻接入层各模块是通过 MakeFile 来编译,因为要导入一些环境变量,所以我将 go test 集成在 MakeFile 中,执行 make test 即可运行该模块下所有的测试用例。
GO = go
 
CGO_LDFLAGS = xxx
CGO_LDFLAGS += xxx
CGO_LDFLAGS += xxx
CGO_LDFLAGS += xxx
 
TARGET =aaa
 
export CGO_LDFLAGS

all:$(TARGET)
 
$(TARGET): main.go
$(GO) build -o $@ $^
test:
CFLAGS=-g
export CFLAGS
$(GO) test $(M)  -v -gcflags=all=-l -coverpkg=./… -coverprofile=test.out ./…
clean:
rm -f $(TARGET) 
注:上述做法,只能生成被测试的代码文件的覆盖率,无奈拿到未被测试覆盖率状况。能够在根目录建一个空的测试文件,就能解决这个问题,拿到全量代码覆盖率。
//main_test.go
package main
 
import (
        “fmt”
        “testing”
)
 
func TestNothing(t *testing.T) {
        fmt.Println(“ok”)
}
流水线加上流程

cd ${WORKSPACE} 可进入当前工作空间目录

export GOPATH=${WORKSPACE}/xxx
pwd
 
echo “====================work space”
echo ${WORKSPACE}
cd ${GOPATH}/src
for file in ls:
do
    if [-d $file]
    then
        if [[“$file” == “a”]] || [[“$file” == “b”]]  || [[“$file” == “c”]] || [[“$file” == “d”]]
        then
            echo $file
            echo ${GOPATH}”/src/”$file
            cp -r ${GOPATH}/src/tools/qatesting/main_test.go ${GOPATH}/src/$file”/.”
            cd ${GOPATH}/src/$file
            make test
            cd ..
        fi
    fi
done
 附录. 材料
·《测试驱动开发》
·《单元测试的艺术》
·《无效的单元测试》
·《重构,改善既有代码的设计》
·《批改代码的艺术》
·《测试驱动开发的三项修炼》
·《xUnit Test Patterns》
·mock 七宗罪

退出移动版