开发者社区 > 博文 > 这段代码你的单测覆盖到了吗?
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

这段代码你的单测覆盖到了吗?

  • 京东零售技术
  • 2021-09-30
  • IP归属:北京
  • 43320浏览
    导读


    在文章开始之前,让我们先问自己几个问题:






    1、你是否曾经因为要修改别人的代码而苦恼?

    2、当别人在修改你的代码时,你是希望骂声一片还是内心点赞?

    3、你是否曾经盯着冗长的方法,复杂的嵌套,花费大量的时间来梳理逻辑?

    4、面对结构混乱,逻辑复杂的代码,你是否想过要重构,但又担心测试的成本太高?


    我相信大部分研发同学在编码生涯中,都遇到过类似的问题,大家都希望看到逻辑清晰,结构简单的代码,都希望代码有完善的测试用例,这样不需要花费太多的精力,就能够更有信心的完成代码修改或重构。

    那么,有什么简单的办法,能够帮助我们做到这些呢?大家应该都猜到了,那就是做好单元测试。

    单测能够帮助我们降低代码复杂度,因为只有代码逻辑更简单,更加原子化、模块化,单测写起来才会更加容易,覆盖率也更容易提升;完善的单元测试,还可以帮助我们在代码修改或重构时,快速的回归验证。

    既然单测能够带来这么多好处,为什么执行起来却这么困难呢?让我们先来采访一下:

    A同学说:单测说起来容易,做起来好难,日常开发工作那么繁重,需求都做不完,我哪里有时间写单侧?

    B同学说:我是很棒的程序猿,我写的代码逻辑缜密,条理清晰,我可以不写单测吗?

    C同学说:我以前是写单测的,后来代码越写越多,业务越来越复杂,我也就逐渐放弃维护单测了。


    经过采访,大家的理由基本一致:1、开发任务太繁重,没时间写单测 2、单测维护成本太高 3、我是研发,不是测试(单元测试可是研发小哥哥的必修课)

    听起来,大家的理由还是蛮充分的,让我们再回到一开始的几个问题:

    1、你在阅读复杂冗长的代码时,是否需要花费大量的时间来梳理逻辑?

    —— 此时如果有完善的单测,梳理起来将会更加快捷清晰,能够帮你节省更多的时间。

    2、面对结构混乱,逻辑复杂的代码,你想重构,会不会担心测试的成本太高?

    —— 此时如果有完善的单测,重构起来将会信心十足,能够帮你节省很多的成本。

    并且,单测可以从源头帮助我们杜绝这些问题,逐步让我们的代码更加简洁清晰。所以我们不要只看眼前的困难,需要看的是长期收益。

    以上说了这么多,主要是希望大家认识到单测的重要性,用心的写好单测,不断提升我们的代码质量。

    下面我们进入正题,带领大家认识单测,写好单测,并介绍不同语言的单测在我们团队中的最佳实践,最后看下如何有效的度量单测。



    什么是单元测试


    单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。单元测试中的单元,可以是C语言中的一个函数,Java语言中的一个类或方法,图形化软件中的一个窗口或一个菜单等。总的来说,单元就是一个最小的被测功能模块。(来自百度百科)

    我们做单测的目的,就是要验证代码里最小的功能模块,确保他的运行结果是正确的,经得起考验的。就好像一辆汽车,他的每个小零件都需要不断打磨和测试,才能放心的组装在一起,整车质量才能有所保障。


    测试模型

    1、冰淇淋模型

    传统的冰淇淋模型也是目前很多团队的主要测试手段。这里面包含了大量的手工测试,端到端的自动化测试,以及极少量的单元测试。这种测试方法造成的后果是,随着业务逻辑越来越复杂,手工回归测试的时间越来越长,可以会被漏测的概率越来越大,质量很难把控。自动化测试一旦失败,调用链路中到底是哪里出的问题,需要花费更大的精力去排查。单元测试又少的可怜,基本起不到作用。


    代码1.jpg


    2、金字塔模型

    Mike Cohn在他的着作《Succeeding with Agile》一书中提出了“测试金字塔”的概念。金字塔模型告诉大家测试是需要分层的,同时也让大家能够直观的感受到,每一层需要投入多少测试精力,以及每一层测试的效率和所消耗的成本。

    越是底层的测试,关联性越小,我们只需要关注单个方法的逻辑,简单的单元测试就很容易做到100%的代码覆盖,这也是最快,最节省资源成本的做法。越往上层,多个单元的集成测试涉及到的业务逻辑越复杂,任何一个分支的变化都可能会成倍的增加测试的复杂度,因此上层的测试效率会更低,遇到问题更难以排查,所消耗的资源成本,时间成本更大。

    我们的底层越牢固,越可靠,上层便越不容易出现问题。因此传统的冰淇淋模型要向金字塔模型转化,我们可以形象的称之为“冰淇淋融化了”。也就是说,最顶部的手工回归测试,要向下融化,优先考虑做全面的单元测试,单元测试覆盖不了的,再进行分层,做服务端的集成测试,集成测试无法覆盖,最后放在UI层。



    为什么要做单元测试


    据统计,大约有80%的程序错误是在软件设计阶段引入的,并且修正一个软件错误所需的费用将随着软件生命周期的进展而上升,错误发现的越晚,修复它的费用就越高,而且呈指数增长的趋势。而单元测试,作为最底层一类测试,是唯一能够保证代码覆盖率能达到100%的测试方法,可以防止开发后期因bug过多而失控,性价比最高。

    《单元测试的艺术》这本书中有这样一个案例:找两个开发能力相近的团队,同时开发相近的需求,我们发现做单元测试的团队编码时长多了一倍,用了14天,但这个团队在集成测试阶段的表现非常顺畅,测试时间更短,bug数量更少,定位bug速度也更快。

    最终我们看到整体交付时间和发现缺陷数量,均是单测团队有绝对的优势。


    代码2.jpg


    单元测试对于京东零售业务系统的重大意义

    上述案例也许和我们的实际场景还有些距离,那么结合京东的真实场景,近两年零售中台业务系统完成大规模的PaaS化改造,整体思路就是降低业务系统的耦合度,将业务系统进行更清晰的垂直业务域、水平业务域的划分,支持业务组件的可插拔可定制,通过扩展点实现个性化业务的快速赋能。PaaS化改造对于京东庞大的业务系统来说,是一次超大规模的重构,各个业务子域的功能模块进行了更细粒度的拆分。PaaS化改造后的各个业务系统模块化程度,可复用程度更高,各模块由于更具独立性,编写单元测试也就更加简单,也更加有必要,单测在此时能够发挥更大的价值。



    如何写好单元测试


    01
    单元测试的编写范围



    首先我们明确下单元测试要做什么,了解了这些,我们才能够更好的进入下一步:

    1、验证行为:用来验证程序中的每一项功能都与期望结果一致。后续加入的研发可以轻松的增加功能或更改程序结构,而不用担心这个过程对程序造成破坏,为代码的重构提供了保障。

    2、驱动设计:编写单元测试帮助我们从调用者角度来观察、思考,尤其是测试先行(test-first),可驱使我们把程序设计得更易于调用,具备可测性,解除软件中的过度耦合。

    3、辅助文档:单元测试是展示函数或者类如何使用的最佳文档,这份文档是可编译、可运行的,并且要维护它保持最新,永远与代码同步。

    4、自动回归:单元测试要具备自动执行的能力,并且不依赖环境和时机,可重复执行,能够随时帮助我们完成自动化的回归验证。


    然后我们需要确认单元测试的编写范围 —— 优先覆盖核心业务、核心应用、核心模块的增量代码,并确保单测通过。

    既然编写单元测试,那优先要覆盖到的肯定是我们的核心代码,这部分代码改动频率较小,功能较为重要,作为单测首选覆盖场景,性价比较高。

    当然,这并不是说其他代码不需要覆盖,因为我们核心的逻辑出错的概率还是比较低的,往往是一些意向不到的地方会出现错误,因此建议增量代码都要做到单测的全覆盖。在进行大的系统重构或改版时,也是编写单测的最佳时机。


    02
    编写有效的单元测试



    我们在编写单元测试时,需要遵循的Automatic(自动化)、Independent(独立性)、Repeatable(可重复) 三个大原则,确保我们的单测可自动执行,没有相互依赖,并且具备可重复执行的能力,不能受外界环境的影响。


    用例的设计要素:

    • 从设计覆盖角度,条件组合>最小线性无关路径>条件>分支>语句。

    • 将内部逻辑与外部请求分开测试

    • 对服务边界(interface)的输入和输出进行严格验证

    • 一定要具备断言的能力

    • 适时使用setup和teardown

    • 原子性,所有的测试只有两种结果:成功和失败

    • 避免随机结果

    • 避免测试中的逻辑,即不该包含if、switch、for、while等

    • 不要保护起来,try…catch…

    • 每个用例只测试一个关注点


    单元测试一定要具备断言的能力,没有判定结果的单测不是有效的单测。

    这种是单元测试吗?


    代码3.jpg


    没有断言、跨层调用,不符合单元测试的基本原则。


    对于涉及外部依赖的系统,如何做好单测?

    我们的系统外部依赖较多,很多的方法返回都依赖外部接口,这时如何来做单元测试呢?

    首先,要明确一点,单元测试只负责方法本身的逻辑正确性,与外部系统的交互属于集成测试和联调测试范畴。对于涉及到外部依赖的方法,很多Mock框架可以帮助我们来完成单元测试,这里列出部分Java语言的Mock框架,可供大家作为参考。


    代码4.jpg


    03
    单测用例设计方法



    单元测试用例的设计和普通用例设计方法基本类似,常见的就是等价类划分、边界值分析等。


    1、等价类划分

    等价类划分法将程序所有可能的输入数据和输出(包含有效的和无效的)划分成若干个等价类,然后从每个等价类中选取具有代表性的数据作为测试用例,从而保证测试用例具有完整性和代表性。形成测试区间的数据不只是函数/过程的参数,也可以是软件可以访问的全局变量,系统资源等,这些变量或资源可以是以数值,也可能是以其他形式存在,如状态。

    例如,计算输入参数绝对值倒数的方法,如果是输入是 0,则抛异常。那么对这个方法设计用例的话,就应该有三个等价类,正数、负数以及0。因此我们可以选取一个正数、一个负数以及 0 来作为三个单元测试用例。


    2、边界值分析

    通常大量的错误发生在输入或输出范围的边界上,而不是发生在输入输出范围的内部。边界值分析法是对输入或输出的边界值进行测试的一种方法,通常边界值分析法是作为对等价类划分法的补充,此时的测试用例通常来自等价类的边界。

    例如,对于上面计算绝对值的倒数的例子,那么边界值就包括 Integer.min、-1、0、1、Integer.max 等。其他类似于空数组、数组的第一个和最后一个、报表的第一行和最后一行等等,也是属于边界值,需要特别关注。


    3、基本路径测试

    基本路径测试法是在程序控制流图的基础上,通过分析控制构造的圈复杂度,导出基本可执行路径集合,从而设计测试用例的方法。设计出的测试用例能够保证程序的每个可执行语句至少执行一次。这种单元测试用例的方法首先要创建出程序的控制流图,之后确定程序的圈复杂度,最后进行测试用例的设计。


    4、错误猜测

    错误猜测法就是根据经验猜想可能的错误,并依此设计测试用例的方法。错误猜测法只能作为测试设计的补充而不能单独用来设计测试用例,否则可能会造成测试的不充分。

    基本思路:列举出程序中所有可能的错误和容易发生错误的情况,根据这些选择测试用例,如:入参为null或者为0等异常情况。


    04
    通过工具提升单测效率



    这里我们主要介绍下京东自研流水线Bamboo平台,除了具备常规流水线的编排能力,对单元测试的运行、单测数据的收集做了很好的支持,目前已经具备Java、C++、Golang、Python、前端、移动端等多语言的单测能力。


    代码5.jpg


    通过简单的配置,就可以将单元测试接入Bamboo流水线,支持定时触发、流水线触发、代码提交触发等多种自动化执行方式,并支持单测报告邮件推送。通过数据统计板块,能够帮助我们掌握部门或项目单测的整体情况。


    代码6.jpg


    05
    多语言场景下的最佳实践



    针对京东多语言的应用场景,我们通过多团队的合作共建,推出了Java、Python、C++、NodeJS、Golang、Android等多种语言的单测最佳实践,并开发出相应的流水线原子插件以支持单测的自动化运行和覆盖率收集,以下将对3种主要开发语言(Java、C++、Python)的最佳实践做一下介绍。


    3.5.1 Java版


    1、框架选择

    工欲善其事必先利其器,选择适合的Java单测框架,将帮助我们事半功倍。

    • 单元测试基础框架:Junit、TestNG

    • Mock框架:Mockito、Jmock、TestableMock、EasyMock等

    • 数据库测试框架:DBUnit

    • Spring测试框架:spring-test

    • 断言框架:Junit、TestNG、Hamcrest


    2、单测执行顺序

    通过注解,我们可以更好的设计单元测试的执行顺序,前置或后置依赖。比如,我们可以在单测方法执行之前,进行数据初始化的操作。

    @Before和@After:环绕型注解,只要有@Test方法执行,都会在@Test方法之前和之后分别执行一次。

    @BeforeClass和@AfterClass:普通型注解,一个类中只能出现一次,在类中只能执行一次。


    代码7.jpg


    3、数据库测试框架

    DBUnit是基于Junit扩展的数据库测试框架,它提供了大量的类对与数据库相关的操作进行了抽象和封装。在80%的情况下,你只需要使用它极少的API,就能够完成相关的测试工作。它通过使用用户自定义的数据集以及相关操作使数据库处于一种可知的状态,从而帮助我们的单元测试能够自动化执行、可重复执行并且相对独立。


    代码8.jpg

    代码9.jpg


    4、参数化测试

    如果需要对数据进行批量测试,我们可以把数据组织起来,用不同的测试数据调用统一的测试方法。参数化测试需要符合以下要求:

    • 参数必须由静态方法data返回

    • 测试类需要标记为:@Runwith(Parameterized.class),并且构造方法参数必须和测试参数相对应


    代码10.jpg

    代码11.jpg


    5、异常测试

    在进行单元测试的时候,我们也经常需要验证一个方法是否正确抛出了异常。异常测试可通过注解@Test(expected = Exception.class)来标识,对于不同的异常类型,可通过expected来进行指定。如下图中expected = NumberFormatException.class。


    代码12.jpg

    代码13.jpg


    6、一些技巧

    数据太大:较大的测试数据可以通过文件的形式来存储,通过读取文件存储来执行我们的单元测试。

    数据制造困难:流量录制可以帮助我们完善测试数据种类,从而快速提高测试覆盖率。

    模拟异常:对于常见的磁盘满,网络断开等异常情况,可通过mock的方式来进行模拟。

    单元测试编写量大:可以借助单元测试生成工具Squaretest自动生成单测代码,然后进行数据补充。


    7、注意事项

    所有的单元测试类最好继承同一个BaseTest,由一个类配置启动Spring容器,否则可能会报bean创建重复异常,无法进行单元测试。


    代码14.jpg


    8、覆盖率插件

    通过插件配置可以帮助我们在本地运行时获取单测覆盖率数据,需要在Java工程根目录pom文件中配置如下两个插件:
































    <!-- 生成单元测试数据插件 --><plugins>  <plugin>    <groupId>org.apache.maven.plugins</groupId>    <artifactId>maven-surefire-plugin</artifactId>    <version>2.22.2</version>   </plugin></plugins>

    <!-- 生成JaCoCo覆盖率数据插件 --><plugin>    <groupId>org.jacoco</groupId>    <artifactId>jacoco-maven-plugin</artifactId>    <version>0.8.2</version>    <executions>     <execution>      <goals>       <goal>prepare-agent</goal>      </goals>     </execution>     <!-- attached to Maven test phase -->     <execution>      <id>report</id>      <phase>test</phase>      <goals>       <goal>report</goal>      </goals>     </execution>    </executions></plugin>


    3.5.2 C++版


    1、框架选择

    GoogleTest也称为GTest,是我们进行C++单元测试的首选框架。首先需要下载GTest框架,并编译安装GTest环境。


    2、Blade编译

    Blade编译方式与CMake编译方式一致,通过一系列的名字为“BUILD”的文件来构建目标,类似于make根据Makefile文件构建目标。

    项目工程结构中,根目录下包含:BLADE_ROOT,依赖项(joft,thirdparty...),项目目录(ump)。必须依赖组件(gtest):thirdparty/gtest。以下是BLADE_ROOT文件示例:


    代码15.jpg


    Blade详细使用可参考:https://usermanual.wiki/Pdf/bladeusermanual.1358018446.pdf


    3、单测编写

    单元测试的目录名utest可以自定义,单元测试的文件名一般以_test.cc结尾,也可以是别的名称,与BUILD中保持一致即可。BUILD文件包含:name,srcs,deps,extra_cppflags,testdata。

    name:编译后单元测试的二进制文件名称

    srcs:源文件,与utest下文件名保持一致

    deps:依赖的lib及文件

    extra_cppflags:编译宏变量设置

    testdata:单元测试用例依赖的数据文件

    以下是BUILD文件示例:


    代码16.jpg


    下面是一个单测的示例,并通过blade进行编译的相关命令:

    环境清理:blade clean

    RELEASE版本编译:blade test -t 32 ... -j32 -p release

    覆盖率版本编译:blade test --gcov ... j8 -p release


    代码17.jpg


    4、数据统计

    C++单测代码覆盖率生成主要依靠gcov和lcov两款工具。下图展示了从编译运行到覆盖率报告生成的过程:


    代码18.jpg


    Gcov —— 产出测试代码覆盖率的工具,伴生GCC,整体工作流如下:


    代码19.jpg


    代码编译:增加编译参数,生成汇编文件及插装

    目标文件生成:产出可执行文件及关联BB和ARC的.gcno文件

    执行可执行文件:插入桩点处收集程序的执行信息并生成.gcda文件,其中有BB和ARC的执行统计次数

    Lcov —— Gcov的图形化前端工具集(lcov,genhtml)。常用的一些数据处理命令如下:

    基准覆盖率数据:lcov-c -i -d "./build64_release/server/" -o base.info

    测试覆盖率数据:lcov --capture --directory './build64_release/server/' --ignore-errors gcov,source,graph -o xx.info

    基准与测试覆盖率数据合并:lcov-a base.info -a xx.info -o total.info

    黑名单过滤:lcov-r total.info `echo $REMOVE_FILE_PATTERN` -o app_remove.info

    白名单提取:lcov-e app_remove.info `echo $EXTRACT_PATH` -o app_extract.info

    html报告生成:genhtml app_extract.info -o cov > cov.log


    3.5.2 Python版


    1、框架选择

    Unittest:是Python标准库中自带的单元测试框架。重要的特性:它通过类(class)的方式,将测试用例组织在一起。unittest不需要单独去下载安装,可以直接使用。

    Pytest:是一个非常成熟的全功能Python测试框架,兼容unittest。

    我们目前比较推荐使用pytest框架,它具备以下优点:

    1、简单灵活,容易上手,文档丰富;

    2、支持参数化,可以细粒度地控制要测试的测试用例;

    3、能够支持简单的单元测试,复杂的功能测试及接口自动化测试;

    4、pytest具有很多第三方插件,并且可以自定义扩展;

    5、有测试用例的skip和xfail处理;

    6、 除了有setup/teardown,还能更自由的定义fixture装载测试用例;


    2、用例规范

    在开始编写单测用例之前,我们需要安装下本地环境:pip install pytest。

    单测用例的编写要符合以下规范:

    • 测试目录以test命名,必须要有__init__.py文件

    • 测试文件以test_开头(以test结尾也可以)

    • 测试类需要以Test开头

    • 测试函数以test_开头(只有符合该规则的函数才会自动执行)


    3、参数化测试

    参数化测试是指把测试数据提取出来,通过参数传递不同数据的方式来驱动用例执行。在Pytest中,我们通过pytest.mark.parametrize()即可实现参数化,既可以对测试方法参数化,也能够对测试类参数化。下图是使用参数化驱动用例执行的示例:













    class TestCase(object):    # argames:指定测试函数例要参数化的形参。列表或字符串形式    # argvalues:定义测试用例(要传给测试函数的实参)。列表格式,列表中的每个元素(元组)对应生成一条测试用例    # ids:同测试固件参数化作用一样,设置每条用例的id    @pytest.mark.parametrize(argnames=['x', 're'],                             # argnames ='x,re',                             argvalues=[(u'你好吗', True), (u'abc', False), (u'123', False)],                             ids=['data0', 'data1', 'data2'])    def test_is_chinese(self, x, re):        """函数参数化装饰器"""        result = is_chinese(x)        assert result == re


    执行结果:


    代码20.jpg


    4、异常测试

    在进行单元测试的时候,有时我们需要测试一个方法是否抛出了正确的异常信息。在pytest中,可使用pytest.mark.xfail()标签实现,如果用例执行失败则结果是xfail,如果用例执行成功则结果是xpass。

    @pytest.mark.xfail(raises=ValueError)

    def test_is_float_xfail(self):

    """采用标记函数进行异常断言"""

    is_float('abcdef')

    上述示例代码中,raises默认None,可指定一个异常类或者异常类元组,表明我们期望用例抛出这些异常。如果用例失败不是因为这些异常,那么用例会执行失败,并标记为FAILED。 


    5、Mock测试

    在遇到以下几种情况时,我们可以考虑使用Mock测试:

    1、依赖内部服务或接口

    2、依赖外部第三方接口 

    3、被测试模块跨多个测试系统依赖,测试环境复杂且不稳定 

    通过Mock可以帮助我们模拟外部依赖,从而使单测的焦点只放在当前函数的功能上。以下介绍两种不同场景的Mock插件:

    外部接口Mock:

    1、安装第三方插件requests_mock,安装命令:pip install requests_mock

    2、通过requests_mock的post方法mock外部接口

    3、调用接口,并得到mock返回值

    目标函数Mock:

    1、安装pytest-mock,安装命令:pip install pytest-mock

    2、可使用三种方式进行mock,三种方式都是使用的mock.patch进行对函数的替换,效果一样

    • 使用pytest-mock中的mocker

    • 使用mock中patch方法,对目标函数的返回值进行替换,采用了with上下文进行管理

    • 使用装饰器的方式对mock对象的函数返回值进行替换

    以下是三种方式的代码示例:







    class TestCase(object):

       def test_clean_attr1(self,mocker):        mocker.patch('src.split_attr.is_chinese', return_value=True)        assert clean_attr([['1', '功能', '10832', '生活', '检测']], ['道具类型'], ['生活']) == []






      def test_clean_attr2(self):        with mock.patch('src.split_attr.is_chinese', return_value=True) :            assert clean_attr([['1', '功能', '10832', '生活', '检测']], ['道具类型'], ['生活']) == []









     # Mock 此方法的时候,必须制定该方法的完整路径    @mock.patch('src.split_attr.is_chinese')    def test_clean_attr3(self, mock_is_chinese):        # mock is_chinese的返回值为True        mock_is_chinese.return_value = True        result = clean_attr([['1', '功能', '10832', '生活', '检测']], ['道具类型'], ['生活'])        assert result ==

       

    6、单测报告

    Python单测报告主要为展示单测用例的执行结果,包括:通过数passed,失败数failed,跳过数skip,错误数error,另外还会有预期异常(expected failures),有xfail标识,以及Unexpected passed,代表有xfail标识,但是执行成功了。

    单测报告目前支持JunitXML和Html两种:

    JunitXML是pytest内置的报告格式,无需安装,格式为xml。生成报告的方式可参考命令:pytest ./test/test_split_attr.py --junitxml=./report/uresult.xml。

    Html报告是第三方的测试报告格式,需要先安装pytest-html插件:pip install pytest-html。生成报告的方式可参考命令:pytest ./test/test_split_attr.py --html=./report/uresult.html 。


    7、覆盖率报告

    Python单测覆盖率统计使用pytest-cov插件来完成,帮助我们在pytest中通过coverage.py来生成覆盖率报告,生成的格式有HTML/XML两种。

    环境准备:pip install pytest-cov

    执行单测、生成单测和覆盖率报告的命令:

    html格式报告:pytest --html=report/ut/utresult.html --cov=./ --cov-report=html:report/cover--cov-config ./.coveragerc-s 

    xml格式报告:pytest --junit-xml=report/ut/utresult.xml --cov=./ --cov-report=xml:report/cover/coverage.xml --cov-config ./.coveragerc -s 

    命令含义:

    --html:生成单元测试报告的路径。

    --cov=:被测代码的路径

    --cov-report=html:report/cover:在report/cover文件夹中生成html格式的覆盖率报告 

    --cov-config:指定使用的配置文件(如果不使用该项,pytest会默认读取执行目录下的 .coveragerc 文件作为配置文件)

    .coveragerc配置文件:配置单测执行统计时的忽略路径或文件等 



    如何度量单元测试


    单元测试的度量按照不同角色关注点的不同,度量的方式也不一样。

    程序猿希望看到的是:我的单测到底覆盖了多少代码,够不够全面?

    领导更希望看到的是:大家投入更多的精力编写单测,到底有没有效果?

    因此,我们可以将单测的度量指标按照过程数据和结果影响划分为过程指标和结果指标。


    01
    过程指标



    在单测的推动过程中,我们重点要关注两个指标:行覆盖率与增量行覆盖率,其中行覆盖率代表了代码的整体单测覆盖情况,而增量行覆盖率则是考量我们新增的代码是否有按要求进行单测。当然,还有一些其他指标可以作为参考:比如分支覆盖率,方法覆盖率,类覆盖率等。


    单测应用占比(主指标):该指标是我们在推动单测的过程中的一个考核指标,目的在于降低当前单测为0的应用占比。

    行覆盖率(主指标):行覆盖率也是在推动单测过程中的一个重要考核指标,度量被测程序中每行代码是否执行,判断标准是行中是否至少有一个指令被执行。

    增量行覆盖率(主指标):与行覆盖率不同的是,增量行覆盖率只考核本次新增代码的行覆盖率,需要有参考基线,目前常用的方式为两次Commit的对比或者两个分支的对比。

    分支覆盖率:度量if和switch语句的分支覆盖情况,计算一个方法里面的总分支数,确定执行和不执行的分支数量。

    方法覆盖率:针对非抽象方法,度量被测程序的方法执行情况,是否执行取决于方法中是否有至少一个指令被执行。

    类覆盖率:每个类中只要有一个方法被执行,则表明该类被执行过,反之则表明该类没被执行。

    字节码覆盖:主要针对Java代码编译后形成的字节码指令,字节码覆盖率表明了在所有指令中,哪些被执行过,哪些没有被执行。


    02
    结果指标



    在结果指标中,我们可以重点关注代码圈复杂度和人均缺陷数量,另外,我们也可以看一些辅助指标,比如提测通过率,平均缺陷解决时长,线上缺陷密度等,短期内我们可能看不到单测所带来的效果,但是长期观测,当我们的代码质量逐步提升,这些指标数据也将会有相应的变化。 


    代码圈复杂度:圈复杂度的值代表覆盖所有路径所需要的最小测试用例数,主要用来评价代码复杂度,以函数为单位,数值越大表示代码的逻辑分支越多,理解起来也更复杂。单测可以有效驱动代码复杂度降低,从而也反向推动单测更容易编写,提升覆盖率。

    提测通过率:当我们在单元测试阶段解决发现了大量的程序问题,基础代码变的健壮,相应的会带来提测通过率的逐步升高。

    人均缺陷数量:单测的提升可以保证代码质量更好,上层测试中发现的问题变少,研发人均缺陷数量相应降低。

    平均缺陷解决时长:单测的全面覆盖可以更有效的帮助排查问题,代码圈复杂度降低也会提高研发排查问题的难度,相应的平均缺陷解决时长也将逐步下降。

    线上缺陷密度:代码质量提高,因代码缺陷造成的线上问题数将会下降,线上缺陷密度也会降低。



    单测的目标


    很多团队可能需要从0到1开始进行单测,也许这个过程是痛苦的,但能够预见到它将给研发团队带来的收益也是巨大的,我们所做的事情是在给未来铺路,因此我们不但要写单测,更要按照正确的方法写好单测,发挥单测应有的价值。

    希望单元测试也逐渐变成我们的工程师文化,也许下次某位同学出现代码级的线上问题时,我们复盘时会多问一句,这段代码你的单测覆盖到了吗?

       


    致谢


    最后,非常感谢以下同学为本文提供的一些最佳实践素材(姓名不分先后):

    李坤然、秦坤、马纯、熊志男、吴俊德




    作者:技数中心韩威