关于单元测试:一台不容错过的Java单元测试代码永动机

37次阅读

共计 7910 个字符,预计需要花费 20 分钟才能阅读完成。

作者:京东批发 陈志良

作为一名京东的软件匠人,咱们开发的软件撑持着数亿的用户,责任是重大的,因而咱们深深地敬畏每一行代码,那如何将咱们的失误降到最低呢?那就是单元测试,它会让咱们建立对代码的自信心。为此咱们冀望能打造一台生产 Java 单元测试代码的“永动机”,源源不断地为开发者生产代码,辅助大家高效地做好单元测试,节俭精力能投入到更多的业务翻新中去。

一、开发者对代码的自信心来自哪里?

京东随着业务高速倒退,咱们缔造的、承载着数亿用户的、功能强大的零碎,在通过十多年的打磨,也变得日益简单。作为 JD 软件开发者,咱们是骄傲的,但咱们承当的责任也是重大的。咱们每一次的翻新,就像打造一座下图这样的过山车。咱们在为客户带来如此顶级体验的同时,更重要的是保障每一次的旅行都能够平安地着陆。所以咱们深深敬畏每一行代码,致力将咱们的失误降到最低,为业务保驾护航。

然而,业务的迭代速度之快,交付压力之大,作为“过山车”的缔造者,你是否有以下的经验?

1)每一次上线也像坐了一次过山车呢?

2)你亲手打造的“过山车”,本人是否亲自体验过呢?

3)你是否曾对测试同学说,“你们先上去坐坐看,遇到了问题再下来找我”?

如果你的答案是:每一次上线也像坐了一次过山车,咱们本人打造的“过山车”本人不敢坐,咱们的代码要靠测试同学兜底,那么就阐明咱们对本人的代码是不足信念的,咱们的工作还有待晋升的空间;反之则阐明,作为一个开发者你曾经相当优良了。

那么如何让咱们开发者建设对本人代码的信念呢,一般来说有两种形式:

1)对“过山车”的每个整机都进行充沛的测试,保障每一部分在各种场景下都能够失常工作,对所有的异样也可能解决切当,这即是单元测试。

2)对“过山车”启动前做好充沛“查看”,这即是代码评审,咱们邀请其余大佬帮咱们把关,及时发现问题。

这两局部工作在开发阶段都是必要的工作,二者缺一不可。

代码评审是借助了外力,单元测试则是内功,靠本人,靠开发者自测来加强对代码的信念

本文次要和大家一起探讨单元测试,如何把这单元测试的内功练好。

二、做好单测,慢即是快

对于单元测试的认识,业界同仁了解多有不同,尤其是在业务变动疾速的互联网行业,通常的问题次要有,必须要做吗?做到多少适合?当初没做不也挺好的吗?甚至一些大佬们也是存在不同的认识。咱们如下先看一组数字:

“在 STICKYMINDS 网站上的一篇名为《The Shift-Left Approach to Software Testing》的文章中提到,如果在编码阶段发现的缺点只须要 1 分钟就能解决,那么单元测试阶段须要 4 分钟,功能测试阶段须要 10 分钟,零碎测试阶段须要 40 分钟,而到了公布之后可能就须要 640 分钟来修复。”——来自知乎网站节选

对于这些数字的准确性咱们暂且持保留意见。大家能够想想咱们理论中遇到的线上问题大略须要耗费多少工时,除了要疾速找到 bug,修复 bug 上线,还要修复因为 bug 引发的数据问题,最初还要复盘,看后续如何能防止线上问题,这样下来激进预计应该不止几人日吧。所以这篇文章作者所做的调研数据可信度还是很高的,

缺点发现越到交付流程的后端,其修复老本就越高

有人说写单测太消耗工夫了,会缩短交付工夫,其实不然:

1)研测同学大量的往返交互比编写单测的工夫要长的多,集成测试的工夫被拖长。

2)没通过单测的代码 bug 会多,开发同学忙于修复各种 bug,对代码 debug 跟踪调试找问题,也要耗费很多精力。

3)前期的线上问题也会须要大量的精力去补救。

如果有了单元测试的代码,且能实现一个较高的行覆盖率,则能够将问题尽可能毁灭在开发阶段。同时有了单测代码的积攒,每次代码改变后能够提前发现这次改变引发的其余关联问题,上线也更加释怀。单测尽管使提测变慢了一些,软件品质更加有保障,从而节俭了后续同学的精力,从整体看其实效率更高。

所以做好单测,慢即是快。

咱们团体技术委员会大佬们从去年开始也在倡导大家做单元测试,

做为一名开发者咱们须要对本人的代码品质负责,

也更能体现咱们大厂开发者的工匠精力。

三、如何编写单元测试

1、单元测试的支流框架及核心思想

以下咱们先通过一个案例介绍下支流框架的思维。下图为一个简略的函数执行逻辑,在函数体内间接调用了函数 1、函数 2、函数 3,间接调用了函数 2.1,其中 1 和 2 别离是一般函数,2.1 和 3 波及到内部零碎调用,例如 JSF、Redis、MySQL 等操作,最初返回后果。

代码大抵如下:

public class MyObject {
    @Autowired
    private RedisHelper redisHelper;


    public MyResult myFunction(InputParam inputParam){MyResult myResult = new MyResult();


// 一般代码块


        if(inputParam.isFlag()) {
            // 如果标记 flag 为 true,则执行函数 1
            String f1 = invokeFunction1();
            // 调用函数 3,函数 3 封装了 redis 中间件操作
            String f3 = redisHelper.get(f1);


            myResult.setResult(f3);


        } else {
            // 调用函数 2,在函数 2 外部又调用近程服务接口 2.1
            String f2 = invokeFunction2();
            myResult.setResult(f2);
        }


        return myResult;
    }  


在当下微服务时代,零碎间的交互变得更加日益简单,以上图例只是简化的例子,理论零碎中的上下游内部依赖多达十几个,甚至几十个。

在这种状况下,如果适度依赖内部服务就很难保障每次用例执行胜利,会影响到单元测试的执行成果。

所以,以后支流的单元测试框架大都采纳了 mock 技术,来屏蔽对外部服务的依赖,例如:mockito、powermock、Spock 等。

图例中 2.1 和 3 即是对外部零碎的调用,单元测试代码中须要将其 API 进行 mock,在用例运行时使用 mock 技术模仿内部 API 接口的返回值,具体写法此处不作举例。

要留神的是,应用 Mock 技术的框架须要留神两个前提:

1)接口契约是绝对稳固的(例如 redis 的 api 临时不会发生变化),否则就须要调整测试用例代码以适应最新的接口契约,如果不调整则此单元测试用例代码是有效的。

2)接口调用是幂等的,同样的入参须要返回雷同的后果,否则用例中的断言会失败或者须要对断言进行非凡的解决,例如比拟时疏忽某些变动的内容(如 id、工夫等)。

2、第 1 种单元测试用例的编写计划

接下来写一段基于 mockito 框架的测试代码,下图中的做法是,开发者编写了一个用例,对外部函数 2.1 和 3 进行了 mock,而后在测试用例中调用待测函数,再对返回值进行断言。

示意代码如下:

    // 创立函数 2.1 的 mock 对象
    @MockBean
    private JSFService myJSFService;


    // 创立函数 3 的 mock 对象
    @MockBean
    private RedisHelper redisHelper;


    @Autowired
    MyObject myObject;


    @Test
    public void testMyFunction(InputParameter parameter)  {


        // 依据入参 mock 返回数据
        when(myJSFService.invoke(parameter.getX())).thenReturn(X);
        when(redisHelper.get(parameter.getY())).thenReturn(Y);


        // 冀望后果
        MyResult expect = new Result(XXX);


        // 理论调用被测试函数,返回后果
        MyResult actual = vmyObject.myFunction(parameter);


        // 断言
        Assert.assertEquals(actual.toString(), expect.toString());


运行该用例后,除了待测函数,连带函数 1、2 一起都被测试到了,在理论中调用链路会更加简单,那么这种写法如何呢?咱们做个简要的剖析:

1)长处:用例的编码量较少,实现速度快,一个用例笼罩了 3 个函数,整个业务执行门路也都被测试到了,另外单测覆盖率的指标不受影响,只有执行过的代码都会被统计到。

2)毛病:如果用例失败,那么去定位问题会较慢,理论我的项目中链路会更加简单,因而排查问题的工夫会大幅度减少,假如问题产生在函数 1 或 2 中,那么就须要通过 debug 跟踪逐渐排查。

那么这样的做法到底如何?到这里如果测试的同学看到必定会有疑难,这样做的用例跟集成测试阶段的自动化用例有啥区别?是的,从成果上看是一样的,只不过将运行转移到了开发阶段。对于排查和定位问题依然比拟艰难,所以从真正的成果登程,不倡议只是这样做,请往下看。

3、第 2 种单元测试用例的编写计划

第 2 种计划是对每一个办法都写用例代码,每个办法是独立的性能单元,隔离该被测办法的全副依赖,将内部依赖的调用都做好 mock。大抵的做法相似下图:

待测函数的测试用例中会波及到 3 个 mock,别离是函数 1、2、3;函数 1、函数 2 也都有本人的测试用例,这样做进去的单元测试成果会更好。在 Java 中办法是一个最小存在的可测试单元,所以对每个办法进行独立的充沛测试,那么组装后就能够充沛保障代码的整体品质,同时也能疾速的定位问题,实现疾速交付。

目前,业界开发者大多采纳第一种偏集成测试的写法,因其工作量绝对较小,在交付压力较大的时候,甚至会放弃单元测试, 这种状况在互联网行业尤为广泛。在单元测试有余的状况下,则须要靠加强测试人员的人力来缓解品质问题,但以后业务增长压力慢慢浮现,各大公司都聚焦于外部提效,人力老本管制更加严格。打铁还需本身硬,当下咱们每一位开发者都须要增强本身的内功修炼。

综合以上两种计划,小结如下:

1)为每个办法写单元测试的测试用例,本办法内部调用均为 mock。

2)编写一小部分集成测试用例,对整体性能进行局部验证,集成测试次要工作还是交给测试同学。

四、单元测试应遵循的一些准则

目前行业比拟风行的有 FIRST 准则,整顿如下

1)Fast,疾速

单元测试用例是执行一个特定工作的一小段代码。与集成测试不同的是,单元测试很小很轻,尽量做到没有网络通信,不执行数据库操作,不启动 web 容器等耗时操作,使它们能疾速执行。开发者在实现应用程序性能时,或者调试 bug 时,须要频繁去运行单元测试验证后果是否正确。如果单元测试足够疾速,就能够省去不必要节约的工夫,进步工作效率。

2)Independent/Isolated,独立 / 隔离

单元测试的用例须要是互相独立的。一个单元测试不要依赖其它单元测试所产生的后果,因为在大多数状况下,单元测试是以随机的程序运行的。另外,用例代码也不应该依赖和批改内部数据或服务等共享资源,做到测试前后共享资源数据统一,能够用 mock 或 stub 的形式对依赖项进行模仿,屏蔽这些依赖项的不确定性,确保单元测试后果的准确性。

3)Repeatable,可反复

单元测试须要放弃运行稳固,在不同的计算机、不同的工夫点屡次运行,都应该产生雷同的后果,如果间歇性的失败,会导致咱们一直的去查看这个测试,不牢靠的测试也就失去了意义。

4)Self-Validating,自我验证

单元测试须要采纳 Assert 相干断言函数等进行自我验证,即当单元测试执行结束之后就可得悉测试后果,全程无需人工染指,不应该在测试实现后做任何额定的人工查看。留神在单元测试中不要增加任何打印日志的语句,防止通过打印出日志能力判断单元测试是否通过。

5)Thorough/Timely,彻底 / 及时

在测试一个性能时,咱们除了思考次要逻辑门路以外,还要关注边界或异样场景。因而在少数时候,咱们除了要创立一个具备无效入参的单元测试,还须要筹备其余应用了有效入参的单元测试。例如被测办法入参有一个范畴,从 MIN 到 MAX,那么应该创立额定的单元测试来测试输出为 MIN 和 MAX 时是否能正确处理。另外就是及时性,等代码稳固运行再来补齐单元测试可能是低效的,最无效的形式是在写好性能函数接口后(实现函数性能前)进行单元测试。

五、单元测试的现状及痛点

1、咱们通过对行业现状进行调研后,有以下发现:

1)从行业特点看:传统行业软件(ERP、CRM 等)单测覆盖率至多达到 80% 以上,互联网行业软件较低,个别低于 50%,大部分没有。

2)从软件特点看:用户量较大的软件(工具类、中间件等)根底软件覆盖率绝对较高,至多 80% 以上,需要变动快的业务类软件绝对较低。

3)从开发习惯看:国外开发的软件较高,更加器重软件的品质,大多数开源软件覆盖率至多都在 60% 以上。国内开发者少数未养成习惯。

2、单元测试这么重要的事件,为什么在企业中理论中却很难做好呢,次要有以下几个痛点:

1)开发者须要投入更多的工作量:一个利用零碎的单元测试代码行数与利用性能代码行数比至多为 1:1,简单利用则更高。通常来说每晋升 1% 的单测行覆盖率,则须要编写业务代码 1% 的测试代码,所以开发者须要付出更多工作量。随着单元测试覆盖率的晋升,每晋升 1%,都须要编写大量的用例,因为后续的用例至多有 80%,甚至是 90% 以上的代码运行门路是重叠的,最坏的状况是减少了一个用例,只多了一行的笼罩。

2)存量代码数量宏大:咱们目前关注的指标还只是外围零碎的覆盖率,全量代码覆盖率晋升更加艰难,经年积攒的利用中放弃代码沉闷的数量仍然很宏大,要做现有代码的单元测试编码须要耗费大量人力。

3)单元测试代码容易生效:单元测试的代码须要继续保护,新业务需要引发的代码变更会导致原有的单测代码生效,在业务高速迭代的状况下,没有额定精力投入,要么疏忽,要么删除,在这种状况下,很难继续维持一个较高的覆盖率指标。

归根结底,单元测试最大的艰难就是老本问题,做好单元测试,咱们的开发者须要继续投入大量的精力,而在业务需要高速迭代的状况下,咱们该如何破局?答案就是: 自动化技术

六、单元测试自动化调研

其实,单测自动化技术的倒退至多已有 15 年以上的历史,目前支流的技术是动态代码剖析技术,它是指无需运行被测代码,仅通过剖析或查看源程序的语法、构造、过程、接口等来检查程序的正确性,找出代码暗藏的谬误和缺点。次要的代表产品有:EvoSuite、Squaretest 等。

上图是 EvoSuite 工具依据现有被测代码主动生成的测试代码,目前这类产品生成的单测代码的行覆盖率个别能够达到 **30%** 左右,代码越简单成果越差,它们能够作为简略业务场景的单测代码生成计划。

次要的长处有:纯客户端工具,装置即可应用,不需简单配置。反对多种开发平台:反对 idea、eclipse、命令行等多种工具。

次要的有余:生成代码品质不高、单测覆盖率较低:受限于代码剖析技术和事实技术框架的简单多样,生成的代码品质不高,单测覆盖率较低,只能实用于简略业务场景,且生成的代码须要人工判断有效性。例如订单 sendpay 这样的标记蕴含了丰盛的业务语义,则很难通过动态剖析生成无效的用例代码。

七、咱们的一些想法与技术冲破

1、将录制的数据转化为单元测试用例

基于动态代码剖析局限性,咱们须要寻找一个新的方向,那么如何可能取得更加丰盛的业务数据呢,而不是通过一些策略生产数据,前年咱们批发交易研发翻新了月光宝盒,齐全能够将数据录制下来,于是咱们就想到是否能够利用宝盒录制到的数据,反向生成测试用例呢,以此来实现疾速生产单元测试的用例代码。大抵的计划思路如下:

2、标杆验证的成果给了咱们信念

乍一听这个想法有点疯狂,咱们针对这个想法做了成果验证,尽管还没有达到奇效,但整体思路失去了测验,事实证明,这个计划尽管很难,然而是可行的,以下为 Y 侧做的标杆案例的尝试。通过 4 个标杆的试运行状况剖析,接入一周内,生成代码 2.3 万行,单测行覆盖率晋升幅度均在 30% 以上。

3、然而,该计划还并不完满,咱们还有些倡议

如果你认真看过后面提到的单元测试准则,针对该计划肯定会有疑难,没错,它违反了及时性准则,咱们应该在写代码时或者提测前实现啊,测试阶段再录制生成曾经晚了。确实,该计划不是完满的,为此咱们给出的倡议是:

1)针对存量代码,因为目前咱们的存量代码数量较大,该计划将会产生较大的成果,开发者只有将录制工具集成到被测利用即可,接入胜利后,如果测试同学能帮忙跑一次全量回归测试最佳,则能够疾速生成大量的用例代码,如果测试同学工夫不短缺,则借助测试同学的日常测试逐步积攒数据,通过一两周后也能取得大量用例代码。

2)对于新开发代码,在开发者实现编码后的自测阶段,由开发者本人本地运行程序进行自测、录制,也能帮忙咱们生成一大批用例,而后能够基于生成的用例,再通过复制、手工调整进行疾速裁减用例,从而保障单元测测的及时性。

3)非凡业务场景解决,对于边界或异样用例很难录制到,则能够通过手工复制用例,再批改用例数据,来裁减用例,这种形式比纯手工编写还是快很多,尤其是 mock 对象非常复杂的时候,用该计划能够在 1 分钟内即可基于已有用例扩大一个新用例。

4、生成的单元测试用例是什么样子

上面举一个生成单元测试用例代码的理论例子,该例子基于 Mockito 框架,每一个用例办法对应一个 JSON 文件,JSON 文件中存储着用例运行时须要的出入参、全副内部调用的数据,用例代码和数据全副由工具主动生成,生成的大部分代码都是在帮忙开发者将录制的数据组装 Mock 对象,这部分工作量在理论开发中是最大的,因而能够大幅度减小开发者本人纯手工编码工作。当须要手工裁减用例时,只须要将用例办法和数据文件复制一份,再对用例数据做出调整即可制作出新的用例。

数据文件样例:
/artt/StockStatusReOccupySplitServiceImpl1#HpCm.json

5、咱们所遇到的技术挑战

咱们遇到了很多技术难点,因为基于宝盒录制的数据在还原代码时信息还有余,须要减少更多的录制信息与非凡利用场景解决,次要难点有:

1)结构化数据的录制与还原,简单泛型的还原、简单对象的序列化和反序列化

2)基于动静代理技术实现代码的非凡解决,如 mybatis、JSF

3)用例的采样管制,反复用例的辨认与剔除,

4)用例后果断言的多样性,须要丰盛的比对策略

期间波及到了大量的底层技术钻研,截至目前咱们依然有很多技术点须要攻克。例如,咱们正在做的利用接入晋升,将 Spring AOP 的形式用 agent+ASM 形式进行替换,实现代码加强在不重启服务的状况下动静挂载、卸载,也进一步升高接入老本,缩小对利用的入侵。

八、单测自动化平台的架构

整体分为三局部:

1)录制端,采纳月光宝盒为基座,基于 Spring AOP 和 ASM 字节码加强 agent 技术,开发者在利用外部进行集成,同时在利用启动中减少 agent 代理脚本设置。

2)平台端,采集到的数据将被发往平台端,平台端次要负责利用注册、录制用例的对立治理等,并为生成端提供用例抽取服务。

3)生成端,以 idea 插件、命令行脚本的模式,为用户的利用生成代码,并且依照每个用例笼罩业务代码的行号进行去重。最终生成的代码提交到代码库,bamboo 集成获取代码进行单测运行与指标的采集。

九、单测平台的共建与接入

单元测试自动化技术是当今软件畛域的一个难题,行业的开发者也都在踊跃寻求冲破

咱们违心做一只啄木鸟

帮忙开发者找到代码里的虫子

通过自动化技术建设单测的信念

但啄木鸟还做不到全面自动化

大家不要因为它的存在而变得懈怠

每位开发者依然要发挥:

工匠精力,以人为本,工具为辅

在提测前轻松做好单元测试

正文完
 0