导读
在《从头到脚说单测——谈无效的单元测试(上篇)》中次要介绍了:金字塔模型、为何要做单测、单测的阶段及指标,在下篇中咱们次要介绍对于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七宗罪