| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 开发测试 -> 从头到脚说单测——谈有效的单元测试(下篇) -> 正文阅读 |
|
[开发测试]从头到脚说单测——谈有效的单元测试(下篇) |
导读 在《从头到脚说单测——谈有效的单元测试(上篇)》中主要介绍了:金字塔模型、为何要做单测、单测的阶段及指标,在下篇中我们主要介绍关于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(是等价类的有效补充) 3. 结合应用 全输入输出,实施难度较大,转而我们思考到业内大神们设计出白盒黑盒设计法,通过仔细思考,可以判断出是对全输入全输出的方法论体现。 因此,白盒&黑盒用例设计法,每一种我都亲自实践,理解其优缺点,从设计覆盖角度,条件组合>最小线性无关路径>条件>分支>语句。 下面这张图,是我早期思考用例设计时的一次实践,现在回忆起来,它过度设计了。 但实际中,我们担心“过度设计”,也还无法给出答案“用什么方法设计保证万无一失”。 ·过度设计,也会使case脆弱 ·在有限的时间内,我们寻求收益较大化 1. 小函数&重要(计算,对象处理):尽量设计全面 2. 逻辑较重,代码行数较多:分支、语句覆盖 + 循环 +典型的边界处理(我们看个例子:GetUserGiftList) 3. 引出“基于实现”与“基于意图”的设计:过多去Stub被测函数内部的调用,就越接近“基于实现”(第二次提到“基于意图”) 十. 基于意图与基于实现这个话题是非常重要的。 基于意图:思考函数最终想做什么,把被测函数当做黑盒,考虑其输出输出,而不要关注其中间是怎样实现的,究竟生成了什么临时变量,循环了几次,有什么判断等。 基于实现:输入输出我也考虑,中间怎么实现的我也考虑。mock就是一个好例子,比如我们写一个case,我们会用mock去验证函数内是否调用了哪个外部方法、调用了几次,语句的执行顺序是怎样的。程序的变动比需求还快,重构随时都有,稍有一变,case大批量失败,这也是《mock七宗罪》中提到的一种情况。 我们要的是基于意图,远离基于实现。 结合实战经验,我总结如下: 1. “要么写好,要么不写”。case也是代码,也需要维护,也有工作量,所以要写的到位,而不是写得多。写了一堆没用的,你还得维护,不如删了。 2. 拿到一个函数,先问问自己,这个函数要实现什么功能,最终输出是什么;然后,问自己,这个函数的风险在哪里,哪部分逻辑不太自信,最容易出错(计算、复杂的判断、某异常分支的命中等)。这些才是我们case要覆盖的点。 3. 内联函数、直接get/set,没几行没什么逻辑的,只要你判断没什么风险,就不用写case。 4. 确定了要写的case,再用分支条件组合、边界等核心方面设计出具体用例,实施编写。 可以结合新闻几次单测case review记录,来详细理解。 我们看一个具体的case: 1. 拿到这个函数,作为测试同学的我先向开发了解该函数的意图:对符合格式、符合时间的用户礼物进行加和 2.?读代码,了解了代码流程、几个异常分支,先做了code review 3.?根据必要的异常分支,设计case覆盖 4. 对正常的业务流程,是按照开发讲述的函数意图,进行设计,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。但是将来是开发来写单测的,他自己设计的函数肯定知道要覆盖哪些异常分支。 2. 嗯,我需要看代码的正常流程是怎样的,但不代表着把代码扒下来以设计出case。case实际上是通过与开发的沟通后,了解输入数据的结构,输出的格式,数据校验和计算的过程,去设计输入输出的。 十一. 用例编写的策略对于怎么个顺序去写单测,我们重点实践了一番,基本上也就三种情况吧: ·独立原子:mockist,被我们推翻了。当然,最底部的函数可能没有外部依赖,那单测它就够了。 ·自上而下(红线):从入口函数往下测。实践的过程中,我发现很难执行,因为我从入口处就要想好每一次调用都需要返回哪些数据及格式,串起来一个case已经非常不易。 ·自下而上(黄线):我们发现,入口函数,往往没什么逻辑,调用另一个函数然后拿到响应返回。所以入口函数,也许不用写?我们继续往下看,每一次调用的函数都看,也调出了以往的线上线下bug,我们发现出现问题的代码部分往往是调用链的底端,尤其是涉及计算、复杂分支循环等。而且,底端的函数往往可测性较好。 因此,考虑两方面,我们选择自下而上设计来选择函数编写case: 1.?底部的函数可测性通常很好 2. 核心逻辑比较多,尤其涉及计算、拼接,分支的。 十二. 可测性问题的解决——重构导致无法写单测的重要原因是,代码可测性不好。如果一个函数八九十行、二三百行,基本就是不可测的,或者说“不好测的”。因为里面逻辑太多了,从第一行到最后一行都经历了什么,各种函数调用外部依赖,各种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七宗罪 关注腾讯WeTest,了解更多热门测试产品:WeTest腾讯质量开放平台 - 专注游戏,提升品质 |
|
开发测试 最新文章 |
pytest系列——allure之生成测试报告(Wind |
某大厂软件测试岗一面笔试题+二面问答题面试 |
iperf 学习笔记 |
关于Python中使用selenium八大定位方法 |
【软件测试】为什么提升不了?8年测试总结再 |
软件测试复习 |
PHP笔记-Smarty模板引擎的使用 |
C++Test使用入门 |
【Java】单元测试 |
Net core 3.x 获取客户端地址 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/17 20:50:17- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |