<p class="f_center"><img src="http://dingyue.ws.126.net/2021/0723/b7df8af8g00qwp7oy000xd200b4001rg00b4001r.gif"/><br/></p><p id="04JJJT3M">作者:ciuwaalu,腾讯安全平台部后台开发<br/></p><p><blockquote id="04JJJTB1">研发效能提升是一个系统化的庞大工程,它涵盖了软件交付的整个生命周期,涉及到产品、架构、开发、测试、运维等各个环节。而单元测试作为软件中最小可测试单元的检查验证环节,可以说是这个庞大工程中最细致但又不可忽视的一个细节因素。本文内容梳理自安全平台部测试效能提升的经验实践,从零开始介绍探讨单测的方法论和优化思路,期望为大家带来参考,欢迎共同交流。</blockquote>什么是单元测试?</p><p id="04JJJT3N">在最开始,我们先看看大家认为的单元测试是什么:</p><p><blockquote id="04JJJTB2">在计算机编程中,单元测试是一种软件测试方法,通过该方法对源代码的各个单元(一个或多个计算机程序模块的集合以及相关的控制数据、使用过程和操作过程)进行测试以确定它们是否符合使用要求。—— 维基百科《Unit testing》</blockquote><blockquote id="04JJJTB3">一个单元测试是一段自动化的代码,这段代码调用被测试的工作单元,之后对这个单元的单个最终结果的某些假设进行检验。单元测试几乎都是用单元测试框架编写的。单元测试容易编写,能快速运行。单元测试可靠、可读,并且可维护。只要产品代码不发生变化,单元测试的结果是稳定的。—— Roy Osherove《单元测试的艺术》</blockquote></p><p id="04JJJT3O">以上这些定义为了严谨起见,都是长长的一大段。在这里,我们结合工程实践经验,给出一个“太长不看”版的定义,这个定义不太严谨但更为简单:</p><p><blockquote id="04JJJTB4">开发同学 在 编码阶段 以 函数方法 为粒度编写测试用例,检验 代码逻辑 的正确性。</blockquote></p><p id="04JJJT3P">在这个一句话定义里,有四个核心要素:</p><p><ul><li id="04JJJT8Q"><br/></p><p id="04JJJT3Q"><strong>角色:开发同学</strong></p><p id="04JJJT3R">单元测试是开发同学工作的一部分,而不是测试同学的工作内容。</p><p><br/></li><li id="04JJJT8R"><br/></p><p id="04JJJT3S"><strong>阶段:编码阶段</strong></p><p id="04JJJT3T">单元测试是在开发编码阶段进行的,而不是转测试之后才开始的。</p><p><br/></li><li id="04JJJT8S"><br/></p><p id="04JJJT3U"><strong>粒度:函数方法</strong></p><p id="04JJJT3V">单元测试主要针对函数方法,而不是整个模块或系统。</p><p><br/></li><li id="04JJJT8T"><br/></p><p id="04JJJT40"><strong>检验:代码逻辑</strong></p><p id="04JJJT41">单元测试主要验证函数方法中的代码逻辑实现,而不是模块接口、系统架构、用户需求。</p><p><br/></li></ul></p><p id="04JJJT42">结合测试 V 型图,可以清晰看到单元测试在项目周期中所处的位置阶段。</p><p class="f_center"><img src="https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2021%2F0723%2Fca9cf495j00qwp7oy000sd200nc00deg00eo008e.jpg&thumbnail=660x&quality=80&type=jpg"/><br/>单元测试有什么好处?<br/></p><p id="04JJJT43">我们不打算罗列《单元测试的N大优势》《写单元测试的N大好处》,只说一条最核心的:<strong>单元测试可以尽早发现编码中的低级错误。</strong></p><p id="04JJJT44">越早发现问题,也越容易解决问题。很显然:</p><p><ul><li id="04JJJT8U"></p><p id="04JJJT45">如果问题在编码阶段、由开发同学通过单元测试发现,开发同学可以<strong>立即修复</strong></p><p></li><li id="04JJJT8V"></p><p id="04JJJT46">如果问题在转测之后、由测试同学发现,可能会走<strong>缺陷单</strong>,修复流程时间长,影响项目进展</p><p></li><li id="04JJJT90"></p><p id="04JJJT47">如果问题在测试阶段未被发现,而在上线后才触发,需要运维同学回滚,甚至可能会导致<strong>现网事故</strong></p><p></li></ul></p><p id="04JJJT48">来自微软的数据,不同测试阶段发现BUG的平均耗时,供参考:</p><p><ul><li id="04JJJT91"></p><p id="04JJJT49">单元测试阶段,平均耗时 3.25 小时</p><p></li><li id="04JJJT92"></p><p id="04JJJT4A">集成测试阶段,平均耗时 6.25 小时 (+92%)</p><p></li><li id="04JJJT93"></p><p id="04JJJT4B">系统测试阶段,平均耗时 11.5 小时 (+254%)</p><p></li></ul></p><p id="04JJJT4C">低级错误造成重大损失的例子实在太多了。有了单元测试,可以避免<strong>面向运气开发,面向回滚发布</strong>,打破“不知道有没有BUG ~ 上线出事回滚 ~ 紧急修复 ~ 代码质量逐渐劣化 ~ 不知道有没有新BUG” 的恶性循环。</p><p>黑盒与白盒</p><p id="04JJJT4D">在软件测试理论中,常常将被测试对象视为一个盒子,这个神秘的盒子接受一些输入,并做某些处理工作,产生特定的输出结果。</p><p class="f_center"><img src="https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2021%2F0723%2F15de6bcfj00qwp7oy0009d2006e00emg003t008p.jpg&thumbnail=660x&quality=80&type=jpg"/><br/></p><p id="04JJJT4E">在构造输入数据进行测试时:</p><p><ul><li id="04JJJT94"></p><p id="04JJJT4F">如果知道盒子的用途,但<strong>不知道盒子的构造</strong>,就是<strong>黑盒测试</strong></p><p></li><li id="04JJJT95"></p><p id="04JJJT4G">如果知道盒子的用途,也<strong>知道盒子的构造</strong>,就是<strong>白盒测试</strong></p><p></li></ul></p><p id="04JJJT4H">白盒测试一般只在单元测试中使用,黑盒测试在单元测试、集成测试等各个阶段都可以使用。</p><p id="04JJJT4I">我们以下方这个函数为例子,看看单元测试中如何应用黑盒与白盒测试。首先需要明确,设计单元测试,我们肯定是知道这个函数的具体用途、输入参数和返回结果的含义(即知道盒子的用途):</p><p></p><p id="04JJJT4J">如果我们手上只有编译好的二进制库文件,不知道函数的内部实现方式,通过想象这个函数在上线后会遇到什么类型的输入,设计了一些合法和非法的 IP 报文来做验证,此时是<strong>黑盒测试</strong>。</p><p id="04JJJT4K">如果我们手上有函数源代码,一边看着函数实现,一边根据代码里的分支、逻辑构造各种输入,此时是<strong>白盒测试</strong>:</p><p id="04JJJT4L">比如看到函数内部的设计了一个空缓冲区的用例;</p><p id="04JJJT4M">比如看到函数内部的设计了缓冲区大小为 19Bytes 的用例。</p><p id="04JJJT4N">在大部分情况下,我们是自己给自己写的函数做单元测试,当运用黑盒测试的思路时,要<strong>假装</strong>被测函数是别人写的。</p><p>覆盖</p><p id="04JJJT4O">在单元测试中,覆盖率是一个常用的评估指标。</p><p id="04JJJT4P">所谓覆盖,可以简单理解为 “被执行过”。具体来说:在某个测试用例中,执行了某行代码,则可以说这行代码“被覆盖”;同样,当某个分支的真/假条件都被取到时,则可以说这个分支“被覆盖了”。</p><p id="04JJJT4Q">常见的覆盖可以分为这几种:</p><p><ul><li id="04JJJT96"></p><p id="04JJJT4R">语句覆盖</p><p></li><li id="04JJJT97"></p><p id="04JJJT4S">分支覆盖</p><p></li><li id="04JJJT98"></p><p id="04JJJT4T">条件覆盖</p><p></li></ul></p><p id="04JJJT4U">假设我们有一个这么一个待测函数:</p><p></p><p id="04JJJT4V"><strong>语句覆盖</strong>是指 每条语句都被执行一次。当输入一组用例时可以达到。</p><p id="04JJJT50"><strong>分支覆盖</strong>是指 每个分支 真/假 条件都被执行一次。当输入以及两组用例时可以达到。</p><p id="04JJJT51"><strong>条件覆盖</strong>是指 每个分支的条件组合方式都被执行一次。当输入(真真)、(真假)、(假真)、(假假)四组用例时可以达到。</p><p id="04JJJT52">语句覆盖是最容易达到、也是最弱的覆盖方式。在工程实践中,考虑到测试成本及测试效果,分支覆盖的覆盖率是最常使用的考察指标。</p><p>桩与驱动</p><p id="04JJJT53">假设我们还有这么一个待测函数:</p><p></p><p id="04JJJT54">调用了外部函数。</p><p id="04JJJT55">假设是一个很重的函数(操作 DB、文件或者网络通信……),进行单元测试时,我们不希望引入这些外部依赖,而是希望调用时立即返回一些提前准备好的“假数据”,这时需要“仿冒”一个,这个伪造过程就叫做<strong>插桩</strong>,假冒的就称为<strong>桩函数(stub)</strong>。</p><p id="04JJJT56">在做测试时,需要写一个函数来调用,这个调用者就是<strong>驱动(driver)</strong>。</p><p>单元测试简单实践 一个简单的单元测试</p><p id="04JJJT57">一个单元测试用例至少包含:</p><p><ul><li id="04JJJT99"></p><p id="04JJJT58">断言</p><p></li><li id="04JJJT9A"></p><p id="04JJJT59">输入数据</p><p></li><li id="04JJJT9B"></p><p id="04JJJT5A">预期输出</p><p></li></ul></p><p id="04JJJT5B">一个简单但完整的单元测试看起来会是这样的:</p><p></p><p></p><p></p><p id="04JJJT5E">Given-When-Then</p><p id="04JJJT5F">单元测试中 被测函数、断言、输入数据、预期输出 几个要素,可以通过经典模板 Given-When-Then(GWT) 来做一些严谨的描述。</p><p><ul><li id="04JJJT9C"></p><p id="04JJJT5G"><strong>Given</strong>描述测试的前置条件或初始状态</p><p></li><li id="04JJJT9D"></p><p id="04JJJT5H"><strong>When</strong>描述测试过程中发生的行为</p><p></li><li id="04JJJT9E"></p><p id="04JJJT5I"><strong>Then</strong>描述测试结束后断言输出结果</p><p></li></ul></p><p id="04JJJT5J">使用 GWT 来描述上一节的用例:</p><p></p><p id="04JJJT5K">有些现代化的测试框架(例如 catch2)对 GWT 描述做了表达上的优化。下方粘贴了一段单元测试代码示例,有对 GWT 更为具体的描述:</p><p></p><p></p><p></p><p></p><p></p><p id="04JJJT5P">组织结构</p><p id="04JJJT5Q">原则:单元测试尽可能以函数方法等<strong>较小粒度</strong>进行组织。</p><p id="04JJJT5R">假设我们有下边一个类,设计单元测试时,最好以各个功能函数为测试目标,而不是将类本身为测试目标:</p><p></p><p></p><p id="04JJJT5T"></p><p id="04JJJT5U">建议:为分别构造不同的测试输入数据。</p><p id="04JJJT5V">不建议:为类构造测试输入数据,然后对使用同样的数据进行单元测试。</p><p id="04JJJT60">常见的测试框架都支持通过测试套件(TestSuite)对测试用例(TestCase)在<strong>逻辑上</strong>进行组织,测试套件可以嵌套,整个单元测试可以组织为树状结构。</p><p id="04JJJT61">常见的测试框架还支持 Fixture。Fixture 是对<strong>测试环境</strong>进行组织,通过函数,以方便进行测试开始前的准备工作,以及测试完成后的清理工作。Fixture 一般会与测试套件结合使用。</p><p class="f_center"><img src="https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2021%2F0723%2Ff0ecb4a8j00qwp7oy0010d200u000fng00it009s.jpg&thumbnail=660x&quality=80&type=jpg"/><br/></p><p id="04JJJT62">组织单元测试的几点准则:</p><p><ul><li id="04JJJT9F"></p><p id="04JJJT63"><strong>轻量:不要有过多的前置条件或外部依赖</strong></p><p></li><li style="list-style: none; display: inline"></p><p id="04JJJT64">轻量的测试用例易于重复执行,方便重现和定位问题。</p><p></li><li id="04JJJT9G"></p><p id="04JJJT65"><strong>独立:同一个测试套件的不同的用例相互独立</strong></p><p id="04JJJT66">测试用例之间尽量独立,避免依赖,可乱序执行,结果稳定复现。</p><p></li><li id="04JJJT9H"></p><p id="04JJJT67"><strong>隔离:使用测试套件隔离资源</strong></p><p id="04JJJT68">使用测试套件与 Fixture 隔离测试用例的资源依赖,以方便管理。</p><p></li></ul>用例设计</p><p id="04JJJT69">设计单元测试用例中有很多方法:等价类划分、边界值分析、路径测试……</p><p id="04JJJT6A">在实践中,我们可以设计覆盖<strong>正常流程 & 异常流程</strong>两大类用例:</p><p><ul><li id="04JJJT9I"></p><p id="04JJJT6B">正常流程通过输入合法的 典型数据、边界值 看<strong>基本功能</strong>是否正确实现</p><p></li><li id="04JJJT9J"></p><p id="04JJJT6C">异常流程通过输入非法数据看<strong>异常处理</strong>流程是否符合预期</p><p></li></ul></p><p id="04JJJT6D">一个函数的内部实现可能是<strong>异常处理-正常流程-异常处理-正常流程</strong>的重复,比如这样:</p><p></p><p></p><p></p><p></p><p id="04JJJT6H"></p><p id="04JJJT6I">因此我们在设计测试用例时,可以:</p><p><ol><li id="04JJJT9K"></p><p id="04JJJT6J">首先设计覆盖<strong>正常流程</strong>的用例,构造一些<strong>合法</strong>的输入:一个典型的 IP 报文,一个有扩展头部的 IP 报文,一个带有 TCP/UDP payload 的 IP 报文……</p><p></li><li id="04JJJT9L"></p><p id="04JJJT6K">其次设计覆盖<strong>异常流程</strong>的用例,构造一些<strong>非法</strong>的输入:空指针,不完整的 IP 头,非 IP 协议……</p><p></li><li id="04JJJT9M"></p><p id="04JJJT6L">最后再考虑一些边界情况:一个不带 payload 的 IP 报文,一个大小为 64K 上限的 IP 报文,一个头部完整但payload 不完整的 IP 报文……</p><p></li></ol></p><p id="04JJJT6M">在设计测试用例过程中,可能会遇到被测函数需要与外部 DB、文件、网络交互的情况,这时候需要使用 Fakes/Stubs/Mocks 进行模拟:</p><p><ul><li id="04JJJT9N"></p><p id="04JJJT6N"><strong>Fakes:包含了生产环境下具体实现的简化版本的对象</strong></p><p id="04JJJT6O">比如模拟的数据库对象、文件描述符、网络连接等。</p><p></li><li id="04JJJT9O"></p><p id="04JJJT6P"><strong>Stubs:包含了预定义好的数据并且在测试时返回给调用者的对象</strong></p><p id="04JJJT6Q">比如很多组预定义好的输入、输出数据,比如数据库查询结果。</p><p></li><li id="04JJJT9P"></p><p id="04JJJT6R"><strong>Mocks:仅记录它们的调用信息的对象</strong></p><p id="04JJJT6S">比如模拟的文件保存接口、数据发送接口等。</p><p></li></ul></p><p id="04JJJT6T">在实践中通常并不纠结这几个词语的区别,常被统称为<strong>插桩</strong>,对应的工具也一般被称作<strong>Mock 工具</strong>。</p><p class="f_center">C++ 单元测试 常见单元测试框架<img src="https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2021%2F0723%2F866d59ccj00qwp7oz0019d200u000dag00it008b.jpg&thumbnail=660x&quality=80&type=jpg"/><br/></p><p id="04JJJT6U">GoogleTest 是老牌测试框架,功能完善,用户很多。<br/></p><p id="04JJJT6V">Catch2 是现代化测试框架,提供了很多特色功能,依赖简单,可以一试。</p><p id="04JJJT70">Boost.Test 是 Boost 自带的测试框架,依赖 Boost 的程序可以直接使用,功能强大。</p><p>一些 Mock 工具<ul><li id="04JJJT9Q"></p><p></li><li style="list-style: none; display: inline"><ul><li id="04JJJT9R"></p><p id="04JJJT73">通过 C++ 多态实现对虚函数进行 Mock</p><p></li><li id="04JJJT9S"></p><p id="04JJJT74">不支持 Free Function 以及非虚函数</p><p></li><li id="04JJJT9T"></p><p id="04JJJT75">目前已经合并为 GoogleTest 的一个子模块</p><p></li></ul></li><li id="04JJJT9U"></p><p></li><li style="list-style: none; display: inline"><ul><li id="04JJJT9V"></p><p id="04JJJT78">通过 Hook 函数入口实现用 Mock 函数无缝替换原始函数</p><p></li><li id="04JJJTA0"></p><p id="04JJJT79">内部开源工具</p><p></li></ul></li><li id="04JJJTA1"></p><p></li><li style="list-style: none; display: inline"><ul><li id="04JJJTA2"></p><p id="04JJJT7C">MySQL 官方提供的服务端 Mock 工具</p><p></li></ul></li></ul>编译参数选项<ul><li id="04JJJTA3"></p><p id="04JJJT7D">开启调试信息:</p><p></li><li style="list-style: none; display: inline"><ul><li id="04JJJTA4"></p><p id="04JJJT7E"></p><p></li></ul></li><li id="04JJJTA5"></p><p id="04JJJT7F">关闭优化和代码保护:</p><p></li><li style="list-style: none; display: inline"><ul><li id="04JJJTA6"></p><p id="04JJJT7G"></p><p></li><li id="04JJJTA7"></p><p id="04JJJT7H"></p><p></li><li id="04JJJTA8"></p><p id="04JJJT7I"></p><p></li></ul></li><li id="04JJJTA9"></p><p id="04JJJT7J">覆盖率:</p><p></li><li style="list-style: none; display: inline"><ul><li id="04JJJTAA"></p><p id="04JJJT7K"></p><p></li><li id="04JJJTAB"></p><p id="04JJJT7L"></p><p></li><li id="04JJJTAC"></p><p id="04JJJT7M"></p><p></li></ul></li></ul>Python 单元测试</p><p id="04JJJT7N">点击阅读。</p><p>小经验分享 三条准则</p><p id="04JJJT7O"><strong>单元测试必须经常跑</strong></p><p><ul><li id="04JJJTAD"></p><p id="04JJJT7P">错误做法:为了完成 KPI 写了一堆测试,跑一次就不管了</p><p></li><li id="04JJJTAE"></p><p id="04JJJT7Q">正确做法:持续集成,自动化运行</p><p></li></ul></p><p id="04JJJT7R"><strong>从增量到存量,从主要到次要</strong></p><p><ul><li id="04JJJTAF"></p><p id="04JJJT7S">从覆盖新模块、新功能做起,单元测试先跑起来再说</p><p></li><li id="04JJJTAG"></p><p id="04JJJT7T">不要追求 100% 的覆盖率,但主要功能逻辑要完成覆盖测试</p><p></li></ul></p><p id="04JJJT7U"><strong>测试用例需要逐步积累</strong></p><p><ul><li id="04JJJTAH"></p><p id="04JJJT7V">上线前已经有了第一批用例,每次迭代都会增加新用例来覆盖变更</p><p></li></ul>实践经验</p><p id="04JJJT80"><strong>思路:以黑盒指导功能验证,以白盒提升覆盖率</strong></p><p id="04JJJT81"><strong>黑盒测试为主:</strong></p><p><ul><li id="04JJJTAI"></p><p id="04JJJT82">黑盒测试验证功能逻辑实现是否正确</p><p></li><li id="04JJJTAJ"></p><p id="04JJJT83">不关心内部实现方式,代码优化重构用例仍可复用</p><p></li></ul></p><p id="04JJJT84"><strong>白盒测试为辅:</strong></p><p><ul><li id="04JJJTAK"></p><p id="04JJJT85">白盒测试关注黑盒测试用例遗漏的分支、路径</p><p></li><li id="04JJJTAL"></p><p id="04JJJT86">可以聚焦于异常处理逻辑是否合理</p><p></li><li id="04JJJTAM"></p><p id="04JJJT87">项目工期紧时可推迟进行</p><p></li></ul>可能踩到的坑</p><p id="04JJJT88"><strong>不要被高覆盖率骗了</strong></p><p><ul><li id="04JJJTAN"></p><p id="04JJJT89">单元测试的目标是发现问题,不是追求高覆盖率</p><p></li><li id="04JJJTAO"></p><p id="04JJJT8A">宏、模板等语法功能可能会使得覆盖率虚高</p><p></li></ul></p><p id="04JJJT8B"><strong>Debug/Release 目标结果不一致</strong></p><p><ul><li id="04JJJTAP"></p><p id="04JJJT8C">Debug 目标关闭优化,启用堆栈保护,某些错误代码可正常执行</p><p></li><li id="04JJJTAQ"></p><p id="04JJJT8D">单测在 Debug 下跑完后,建议在 Release 下再跑一次</p><p></li></ul></p><p id="04JJJT8E"><strong>代码合并导致单测失败</strong></p><p><ul><li id="04JJJTAR"></p><p id="04JJJT8F">小A和小B分别开发新功能,push 前单测都通过了,MR 后单测却挂了</p><p></li><li id="04JJJTAS"></p><p id="04JJJT8G">使用持续集成发现问题</p><p></li></ul>提高代码的可测性</p><p id="04JJJT8H">在编码过程中,多多考虑代码的可测性,可以让单元测试事半功倍:</p><p><ul><li id="04JJJTAT"></p><p id="04JJJT8I">开发过程及时编写测试用例,<strong>边开发边测试</strong>,不要等全部开发完毕了才开始写测试用例</p><p></li><li id="04JJJTAU"></p><p id="04JJJT8J">函数功能简单,避免随机性,以免测试结果不稳定</p><p></li><li id="04JJJTAV"></p><p id="04JJJT8K">函数减少输入输出,使简单的输入数据组合可以完成测试覆盖</p><p></li><li id="04JJJTB0"></p><p id="04JJJT8L">遵循 SOLID 原则</p><p></li></ul></p><p id="04JJJT8M"><strong>最后</strong></p><p id="04JJJT8N">在实际研发与测试工作中,单元测试是保证代码质量的有效手段,也是效能优化实践的重要一环。安平研效团队仍在持续探索优化中,若大家在工作中遇到相关问题,欢迎一起交流探讨,共同把研效工作做好、做强。</p>
讯享网

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/157662.html