当前位置: 首页 > 后端技术 > Java

从头到脚谈单元测试——谈有效的单元测试(下篇)

时间:2023-04-01 22:27:14 Java

Introduction主要在《从头到脚说单测——谈有效的单元测试(上篇)》介绍:金字塔模型,为什么要做单元测试,单元测试的阶段和指标,下篇我们主要介绍更多关于mocks的精彩内容,如何不滥用mocks,写用例的策略等等,一起来看看吧~7.必须说说mocks。Testdoubles是在《xUnit Test Patterns》一书中,作者首次提出了testdoubles(测试替身)的概念。我们常说的mock只是其中之一,也是最容易和Stub(打桩)混淆的。上一节gomonkey的介绍大家可以注意到我没有使用mock,都是Stub。是的,gomonkey不是一个mock工具,而是一个高级的打桩工具,可以适应我们大部分的使用场景。测试替身有五种类型:DummyObject用于传递给调用者但永远不会被实际使用,通常它们只是用来填充参数列表TestStubStub通常用于在测试中提供封装的响应,例如,有时编程不会响应所有呼叫。Stubs也会记录调用,例如电子邮件网关就是一个很好的例子,它可以用来记录所有发送的消息或它发送的消息数量。简而言之,Stub一般是对真实对象的封装。TestSpyTestSpy就像一个间谍,插在SUT内部,负责将SUT内部的间接输出传递给外部。其特点是内部间接输出返回给测试用例,由测试用例进行验证。TestSpy仅负责获取内部信息并向外发送信息,不负责验证信息的正确性。MockObject为设置调用的方法和需要响应的参数封装一个合适的对象·FakeObjectFakeobjects往往与类的实现一起工作,但只是为了让其他程序正常运行,比如一个in-内存数据库就是一个很好的例子。Stub和mock打桩和mock应该是最容易混淆的了,我们习惯用mock来形容模拟返回的能力。习惯了很自然,所以我们常说mock。据我理解,stub可以理解为mock的一个子集,mock更强大:Mock可以验证执行过程,验证一个函数是否执行,执行了多少次Mock可以根据条件生效,比如传入特定的参数,会让mock效果生效。Mock可以指定返回结果。当mock指定任何参数返回固定结果时,等于stub。但是go的mock工具gomock只是基于接口生效,不适合新闻和企鹅项目。gomonkey存根涵盖了大多数使用场景。八。不要滥用模拟。我把这部分单独放在一章来说明它的重要意义。需要了解小鹏的《mock七宗罪》,在gitchat上。两派大约在2004-2005年间,江湖上形成了两派:经典测试驱动开发派和mockist(模拟极限派)。先说mockist。他提倡模拟被测函数调用的所有外部函数。也就是说,只关注被测函数本身的代码行,只要调用其他函数,都是用假数据mock测试的。再来说说经典的测试驱动开发学派。他们提倡不要滥用模拟。如果你不能嘲笑,你就不能嘲笑。被测单元不一定是一个特定的功能,它可能是多个功能,串在一起。必要时嘲笑。两大宗门争斗多年,各有利弊,时至今日依然并存。存在是合理的。比如mockist使用的mock过多,无法覆盖函数接口,这部分很容易出错;classicschool,字符串太多,被质疑为综合测试。对于我们的实际应用来说,不必强行追随某个派系,结合起来就好,必要的时候mock,尽量少mock,不要着急。什么时候适合嘲笑?如果对象具有以下特征,则更适合使用模拟对象:对象提供非确定性结果(例如当前时间或当前温度)对象的某些状态难以创建或重现(例如网络错误或文件读写错误)对象方法执行太慢(如测试开始前初始化数据库)对象尚不存在或其行为可能改变(如在测试驱动开发中驱动一个新类)的object中必须包含一些专门为测试准备的数据或方法(后者不适用于静态类型语言,流行的Mock框架无法为对象添加新的方法,stub是可以的。)因此,不要滥用mock(存根),当被测方法调用其他方法函数时,第一反应应该是进去串起来,而不是从根上mock。九。用例设计方法我看了一篇文章:像机器一样思考文章讲述了思考编程的基本思想——考虑输入和输出。我们在设计案例的时候,希望得到最全面的设计。我们基本上考虑全输入和全输出的组合。当然,一方面,这样做太费时间,而且往往无法执行;另一方面,这不是期望的结果,要考虑投入产出比。这时,就需要理论与实践相结合,理论指导实践,实践精品理论。先说理论还是从上一篇说起。考虑输入和输出,首先要知道哪些属于输入输出:whitebox&blackboxdesign。白盒法:逻辑覆盖(语句、分支、条件、条件组合等)路径(全路径、最小线性无关路径)循环:结合5种场景(skiploop、looponce、loopmaxtimes、loopmhits、loopmmisses)黑盒法:等价类:correct,wrong(legal,illegal)boundarylaw:[1,10]==>0,1,2,9,10,11(是对等价类的有效补充)结合全输入全输出的应用,实现起来难度更大,那么我们就思考一下业界大神们设计的白盒和黑盒设计方法。经过仔细思考,我们可以判断它是一种全投入全输出的方法论体现。因此,我亲自实践了每种白盒&黑盒用例设计方法,以了解它们的优缺点。从设计覆盖率来看,条件组合>最小线性独立路径>条件>分支>语句。下图是我早期在思考用例设计时的一个实践。现在回想起来,它是过度设计的。但在实践中,我们担心“过度设计”,也一直无法给出“用什么方法设计才能保证万无一失”的答案。过度的设计也会使表壳易碎。在有限的时间内,我们力求利益最大化。Smallfunctions&important(computation,objectprocessing):尽量设计综合逻辑重,代码行数多:分支,语句覆盖+循环+典型的边界处理(我们看一个例子:GetUserGiftList)导致“基于实现”以及“基于意图”的设计:被测Stub函数内部调用过多,越接近“基于实现”(第二个提到“基于意图”)十。基于意图与基于实现的主题非常重要。Intention-based:思考这个函数到底要做什么,把被测函数当成一个黑盒子,考虑它的输出和输出,而不是去关注它中间是怎么实现的,有哪些临时变量生成,循环多少次,有什么判断等等。基于实现:我也考虑输入和输出,我也考虑中间怎么实现。模拟就是一个很好的例子。比如我们在写案例的时候,会通过mock来验证函数中是否调用了哪个外部方法,调用了多少次,语句的执行顺序是怎样的。程序变化快于需求,重构随时发生。如果稍有变化,就会导致大量案例失败。这也是《mock七宗罪》中提到的一种情况。我们希望基于意图,而不是基于实施。结合实战经验,我总结如下:“要么写好,要么不写”。case也是代码,同样需要维护和工作量,所以需要写到位,不能写太多。写了一堆没用的,还得维护,不如删除。当你拿到一个函数的时候,先问问自己,这个函数要实现什么功能,最终的输出是什么;然后问问自己,这个函数的风险在哪里,哪部分逻辑没有信心,最容易出错(计算、复杂判断、命中异常分支等)。这些是我们的案例将涵盖的要点。内联函数,直接get/set,几行没什么逻辑,只要判断没有风险就不用写case了。确定要编写的案例,然后利用分支条件组合、边界等核心方面设计具体的用例并实施编写。结合新闻中的几条单测用例审核记录可以详细了解。下面看一个具体的案例:拿到这个函数,作为测试同学,首先从开发者那里了解到这个函数的用途:添加满足格式和时间的用户礼物2.阅读代码,理解代码流程,几个异常分支,先做一个codereview3。根据必要的异常分支,设计用例覆盖正常业务流程,根据开发中描述的功能意图进行设计,用例如下:单个测试用例funcTestNum_CorrectRet(tofthenormalpathofthetestedfunction*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,}vars*redis.xxxpatches:=gomonkey.ApplyMethod(reflect.TypeOf(s),"Getxxx",func(_*redis.xxx,_string)(map[string]string,error){returngiftRecord,nil})延迟补丁.Reset()p:=&StarData{xxx}userStarNum,err:=p.GetNum(10000)assert.Nil(t,err)assert.JSONEq(t,Calorie.StructToString(expectRet),Calorie.StructToString(userStarNum))}有同学会问:但是你最后还是看代码了?看看代码的正确逻辑是如何处理的,然后设计案例,构造数据?而且不看代码怎么知道覆盖哪些异常分支呢?答:1.作为一名测试人员,我现在正在写一个开发同学的案例。我确实需要知道处理哪些异常分支,但不限于代码中的几种,还应该包括我理解的异常分支,应该在案例中体现出来。中间。我们的案例绝不是为了证明代码是如何实现的!通过单机测试,我们往往可以发现bug。但是以后会发展到写单元测试。他设计的函数必须知道覆盖哪些异常分支。好吧,我需要看看代码的正常流程是什么样的,但这并不意味着代码被剥离来设计一个案例。案例实际上是通过与开发者沟通,了解输入数据的结构,输出的格式,以及数据校验和计算的过程来设计输入输出的。11.用例编写的策略至于写单机测试的顺序,我们重点练习了,基本上有三种情况:独立原子:mockist,被我们推翻。当然最底层的函数可能没有外部依赖,单独测试一下就够了。·自上而下(红线):从入口函数向下测量。在实践的过程中,我发现实现起来比较困难,因为每次从入口调用都要弄清楚需要返回什么数据和格式,很难串一个case。自下而上(黄线):我们发现入口函数经常没有逻辑,调用另一个函数然后得到响应。那么入口函数,也许你不需要写呢?我们继续往下看,看看每次调用的函数,把之前上线下线的bug都调出来。我们发现有问题的代码部分往往是调用链的底层,尤其是涉及到计算、复杂的分支循环等,而且底层的函数往往更容易被衡量。因此,考虑到两个方面,我们选择自下而上的设计来选择函数编写案例:1、底层函数的可测试性通常较好,核心逻辑较多,尤其涉及计算、拼接、分支等。12、解决可测试性问题——重构导致无法编写单元测试的重要原因是代码可测试性不好。如果一个函数有八十、九十行,两三百行,那基本上是不可测的,或者说“不可测”。因为里面的逻辑太多了,从第一行到最后一行经历了什么,各种函数调用外部依赖,各种if/for,各种异常分支处理,写一个case的代码行数可能是原来的功能好几倍。因此,要推动单体测试,重构以提高可测试性是必须的。而且,通过重构,代码结构间接清晰,可读性和可维护性更强,更容易发现和定位问题。常见问题:重复代码、幻数、箭头代码等。推荐的理论书籍是《重构:改善既有代码的设计》第二版,《clean code》我输出了一篇关于重构的文章。使用codecc(腾讯码检中心)的圈复杂度和函数长度来评价代码结构的好坏。我们在发展中一起学习、一起实践,不断出成果。对于箭头式的代码,可以考虑以下步骤:1.多使用guard语句,先判断异常,返回异常2.提取判断语句3.将核心部分提取到一个函数中十三。用例维护性、可读性、可维护性和可靠性用例设计元素测试内部逻辑和外部请求严格验证服务边界(接口)的输入和输出使用断言代替原生错误报告功能避免随机结果尽可能避免断言时间的结果·及时使用setup和teardown·测试用例相互隔离,互不影响·原子性,所有测试只有成功和失败两种结果·避免测试中的逻辑,即,它不应该包含if、switch、for、while等。不要保护它,try...catch...每个用例只测试一个关注点。少用睡眠,拖延考试时间的行为是不健康的。3Astrategy:arrange,action,assert用例的可读性标题要明确说明意图,比如Test+被测函数名+条件+结果。案例失败后,通过名字就可以知道是哪个场景失败了,不用一行行看代码。以后维护这段测试代码的可能是其他人。我们需要让其他人容易理解。测试代码的内容要清晰。3A原则:arrange、action、assert分为三部分。如果arrange的数据准备部分代码行多,可以考虑拉出来。·断言意图明显,可以考虑把幻数变成变量,名字通俗易懂·一个案例,不要做太多断言,要具体·与业务代码要求一致,它必须是可读和可维护的·重复:文本字符串重复、结构重复、语义重复·拒绝硬编码·基于意图的设计。不要因为业务代码重构一次,导致一批案例失败。注意代码的各种臭味,看《重构》第二版用例可靠性单元测试,小而快,不是为了发现这个时候Bugs,设计成放在流水线上,找出每个MR是否有一个错误。单元测试失败的唯一原因应该是bug,而不是因为不稳定的外部依赖、基于实现的介入等。长期失败将失去单元测试的警告功能。“狼来了”的故事是一个沉痛的教训。·未经测试的程序缺陷,随机失败案例·从不失败案例·没有断言的案例·命名错误的案例十四。新闻单元测试的推进过程我们提到单元测试的实践分为4个阶段,每个阶段都有目标。第一阶段会写,全员写,不要求文笔好·自上而下,从总监到组长,大力支持,毫不犹豫,让组员情绪高涨·快速确定单测框架和熟练使用·结合开发需求,输出单测框架在各个场景下的使用方法,包括assert、mock、table-driven等。封装http2WebContext,方便生成context对象多次训练讲解单测测试理论和框架用Teams(终端,接入层)指定一个单测接口人,他先尝螃蟹。他是最熟悉框架的使用,也是前期写案例最多的人。单测框架集成使用后,部分同学会试用,保证连续两次迭代。这些学生都有案例输出。在每次迭代的汇总数据中,添加单测的相关数据:组长和总监非常关心单测的数据信息,鼓励他们增加案例数和单测行数代码。与团队分享mocks的正确使用和用例设计的正确思路,讨论后达成一致。结对编程,每次迭代结对2-3个开发,一起写案例,互相改进。这里的配对灵活:有的开发,只需要半天时间给他讲解框架的使用,和他一起练习,他就可以放心上手;写完case,开发review学习,尝试写你的第一个case;有的开发者一开始可能不接受,以需求不适合单测为由观察一段时间。写起来没那么难,对团队也有好处。他甚至会主动找考试同学教他写案例。·测试同学审核开发提交的案例,跟进开发修改后重新MR·连续两次迭代,邀请dot老师和乔钢柱进行案例审核,效果很好·分析迭代单测数据,关注需求覆盖、人员覆盖、案例增量。团队负责人继续鼓励和支持单元测试。每次迭代的需求中增加“单元测试”字段,由组长评估后设置。没有单测的MR是不会通过的,单测也会被review。可测性提升第三阶段·测试和开发一起学习《重构》第二版,每周会有分享会·部分重点同学优先重构自己代码和测试同学要求严格,首先保证有一个单测,然后小步重构,每一步都有单测保证。通过流水线的codec扫描后,圈复杂度和函数长度必须达标,不允许人工干预。通过第四阶段TDD。它不保证开发人员可以做TDD。门槛还是挺高的,需要线下熟练,才能应用到业务开发中。逐步推进开发同步编写业务代码和测试代码,而不是在补充案例·测试学员练习TDD后完成业务代码15.管道的单机测试应该在管道上运行,客户端和后台都配备管道以确保每个推送和MR都运行一次并发送报告。对于go的单体测试,新闻访问层的各个模块都是通过MakeFile编译的,因为需要引入一些环境变量,所以我将gotest集成到MakeFile中,执行maketest运行该模块下的所有测试用例。GO=去CGO_LDFLAGS=xxxCGO_LDFLAGS+=xxxCGO_LDFLAGS+=xxxCGO_LDFLAGS+=xxxCGO_LDFLAGS+=xxxTARGET=aaaexportCGO_LDFLAGSall:$(TARGET)$(TARGET):main.go$(GO)build-o$@$^test:CFLAGS=-gexportCFLAGS$(GO)测试$(M)-v-gcflags=all=-l-coverpkg=./...-coverprofile=test.out./...clean:rm-f$(TARGET)注意:上面在实践中只能生成经过测试的代码文件的覆盖率,无法获取未测试的覆盖率。您可以在根目录中创建一个空的测试文件来解决这个问题并获得完整的代码覆盖率。//main_test.gopackagemainimport("fmt""testing")funcTestNothing(t*testing.T){fmt.Println("ok")}pipelineplusprocesscd${WORKSPACE}可以进入当前工作区目录exportGOPATH=${WORKSPACE}/xxxpwdecho"=======================工作空间"echo${WORKSPACE}cd${GOPATH}/srcforfileinls:doif[-d$file]thenthenif[["$file"=="a"]]||[[“$文件”==“b”]]||[[“$文件”==“c”]]||[["$file"=="d"]]然后echo$fileecho${GOPATH}"/src/"$filecp-r${GOPATH}/src/tools/qtesting/main_test.go${GOPATH}/src/$文件“/”cd${GOPATH}/src/$filemaketestcd..fififidone附录。资讯·《测试驱动开发》·♂·《mock七宗罪》·《测试驱动开发的三项修炼》·《xUnit Test Patterns》·模拟七大罪