聊聊单元测试

作为一名质量管理人员,从刚入行时就接触到单元测试:需求提测时要保证一定的单元测试覆盖率作为提测准入;进行线上问题case study时会问,这个bug单测是否可以发现;还有各种质量度量。对于单元测试的意义,绝非一个指标或几个指标可以度量的,或许你看到的指标只是一个Trick。

单元测试测什么

对于单元测试测什么,怎么写,貌似很多同学甚至研发也搞不清楚,每次要进行CI发现好多单测不通过,错误原因五花八门,诸如:依赖的下游服务调用失败,mysql连不上或者由于脏数据引发的失败,读写文件找不到,一些中间件连不上等等。这些都造成单测维护困难,修复无果,大多数情况都被注释掉了。
对于单元测试测什么,那我们先弄清单元是什么的问题。笼统的说,单元是具有独立功能的最小单位,如果你使用函数式编程,它可能是一个函数,如果是面向对象语言,如java、c++,它可能是一个方法、一个类。如果是一个java web项目,单元可以是一个util类、一个dao方法、一个service方法、甚至一个controller方法。 单元测试就是从基本功能、边界值、状态、异常处理等角度设计测试用例对单元进行测试。
有人或许有疑问,service层一般会调用util层,那我是不是直接测试service就可以了,不用再测试service了?当然不是,单元测试需要是自下而上的测试,依赖基础功能不能保障,肯定不能保障上层的测试。另外通常是先编写util再编写service,util编写完成时就应该同时完成util的单元测试,这是设计上的原则。再就是util可能被多个service调用,每个service的场景输入或许不同,通过单个service的测试不能保障util功能测试场景的完备性。
为了避免对于单元定义的分歧,google用小型测试(见下图)描述单元测试,所以当你写单元测试时,可以自检确定是否是单元测试。


在这里插入图片描述

单元测试的意义

当我们提倡单元测试时,经常有些质疑的声音:“本来开发任务就重,哪有时间写单元测试,我们写了你们测试还做啥?”,有时甚至我也质疑:“我们单元测试覆盖率有80+%了,为啥还这么多bug?”
其实要让单元测试充分发挥它的作用,需要多种因素:写,研发要有种高度自驱的精神尽量保证单测覆盖;review,团队之间要互相敦促,创建一个美好的研发习惯和氛围;维护,单测失败了或代码修改了一定要像哺育自己的孩子一样牵挂着单测;统计,在看板上对于单测的覆盖情况和运行情况实时更新,一目了然。这几个因素,只有review可以忽略外,其它因素缺一都会无法保障单测的意义和效果。
单元测试的核心意义在于,良好的可测性代码设计,研发代码质量保障。
关于可测性设计,我们先来说TDD,基于测试驱动的研发模式,是要研发人员在写代码前先完成单元测试,之后为了保证单元测试通过,进而去完善代码逻辑。这种研发模式比较理想,现实中很难有研发按照这种模式去做。但是尽管不能做到测试先行,退而求其次,在开发过程中也要铭记测试,为了能较好的进行测试,要保证逻辑高内聚,低耦合的同时,尽量为测试提供灵活的支持。例如下例就不具备一个较好的可测性设计。

public static String getTimeOfDay() {

    Calendar calendar = GregorianCalendar.getInstance();
    calendar.setTime(new Date());
    int hour = calendar.get(Calendar.HOUR_OF_DAY);

    if (hour >= 0 && hour < 6) {
        return "Night";
    }
    if (hour >= 6 && hour < 12) {
        return "Morning";
    }
    if (hour >= 12 && hour < 18) {
        return "Afternoon";
    }
    return "Evening";
} 

对单元测试、集成测试、系统测试等诸多测试而言,单元测试首当其冲,从实现成本、问题修复成本角度看单元测试也是最低的。据统计85%的bug可以在单元测试阶段发现。对于单元测试的整体收益看,要大于研发中编写单元测试耗费的代价。


在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

如何编写单测

单元测试应该是简单无依赖的,包括数据库、网络、磁盘、下游接口、中间件服务等,都不应该阻碍单元测试的运行。所以代码可测性需要解决的问题之一就是要求要解决方便对这些依赖进行隔离或者mock。例如下例不是具备好的可测性,由于UserService的耦合关系,导致不方便构造userservice的stub服务。

Public class TaskService { 

    public  List<Task> getTask(User user) {

        UserService userService = new UserService();
        
        if (!userService.isValidUser(user)){
            return null;
        }
        return taskDao.getTask(user);
        
    }
}

在mock工具不是很完备的很长一段时间,开发人员需要通过代码编写此类的stub服务,造成单元测试编写和维护代价非常高,这也是单元测试覆盖率不高的原因。

Public class TaskService { 
    UserService userService;
    
    Public TaskService(UserService service) {
        this.userService = service;
    }

    public  List<Task> getTask(User user) {
        
        if (!userService.isValidUser(user)){
            return null;
        }
        return taskDao.getTask(user);
        
    }
}
Public class UserServiceStub implements UserServiceInterface{ 
    
    public  boolean isValidUser(User user) {
        
        return true;
        
    }
}

所幸我们站在各种巨人的肩膀上写代码,现在各类语言都有自己的mock工具,比如java就有mockito、powermock等很方便的mock。尽管如此,还是要以代码可测性为前提。

单测的局限

通常单元测试的行覆盖率可以达到80%甚至90%,那么我们是不是可以不进行其它测试了呢?当然不是,如果真是那岂不是测试人员要失业了。在敦促单元测试的同时我们必须认识几点:

  • 单纯的高行覆盖率是无效的
    除了行覆盖率外,通常的单测覆盖率插件会提供多种覆盖率统计数据,其中包括分支覆盖率(圈覆盖率),是指代码分支的覆盖 / 所有代码分支情况。也就是同一段逻辑,虽然保证行覆盖率,但不能保证所有情况都能准确走到该代码块中,也就是高的行代码覆盖率,不一定具备高的分支覆盖率,这也是为什么高的行覆盖率情况下依然有bug的原因之一。
  • 单元测试的局限性
    上面说过,单元测试应该是不依赖数据库、多线程、网络、接口、文件等,在单元测试中我们尽量使用mock将这些隔离,假的终究是假的,所以总会因为其中的差异导致bug钻了空子,例如我们用h2内存数据库mock mysql数据库,那我们要从sql语法规则、mock数量、数据场景(分库分布)、数据分布等去考虑,所以单元测试通常是发现简单少量数据场景下的基本功能问题,对于大量数据复杂场景的并发问题需要进行接口测试、系统测试、异常测试、压力测试、安全测试。

测试人员在单测中的职责

单元测试是研发人员编写,那测试人员就无所事事了么?当然不是。测试人员同样要具备编写单元测试的能力,提供单元测试所需的各种脚手架,对单测的有效性进行review,对单元测试覆盖进行监督和度量。
当你所在的团队都认可单测的意义、不再为写单测找借口时,当你不再经常对基础的本该单测覆盖的功能反复测试时,当团队不再为重构而担忧时,单测才走上一个较成熟的阶段。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342