各位都是怎么进行单元测试的?请大神来指导

发表于:2021-6-16 09:18

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:大宽宽    来源:知乎

分享:
  刚好在搞测试规范,顺便谈谈单元测试
  首先我先梳理下概念。因为比如像“单元测试”这个词在不同场景下可能有不同意思。我更偏向于将大家大致理解的“单元测试”称为“开发阶段的自动化测试“。这样就可以体现三个点:
  一是,这个测试是开发同学自己搞的。开发人员搞定代码后,为新代码添加测试,并且保证新老测试都能通过,才能提测。开发同学不能将满是bug的代码就丢给测试同学。
  二是,这个测试是自动化的,不是“手工调用接口做一次测试“那种形式的。并且执行起来要非常简单。比如任何人拿到代码,只要机器上装了对应的开发工具,都可以轻松运行测试,看到报告(比如对于java,一句mvn test, 或者对nodejs,一句npm test就能跑)。
  三是,开发人员要做的测试,真的不一定是只验证一个“单元”的正确性(下面详细讲)。
  市面上大部分关于这种测试的介绍,大多集中在某个工具或者框架怎么用(比如JUnit,mockito,jest等);要不就是一些理论上的对这种测试好处的介绍,通篇是“应该做测试”,“这么做了就容易得到高内聚的代码“等等。但是很少有关于真正为什么这个事情难以落地的讨论。这也是我很想多唠叨两句的地方。
  最郁闷的是很多人一开始写测试代码就错了,因为压根就没搞明白要测试什么,保证什么东西是对的。这个无论什么工具都不帮不了你,只能由开发者自己思考——对于某个特定的测试代码,花那么大力气去写,去mock,去assert,到底想验证什么。一个不留神,测试代码就变成验证比如JPA的接口对不对,某个第三方系统库的函数对不对等等。这明显不是聪明的做法。
  最常见的测试目标是是验证“自己写的一小段代码是不是符合设计逻辑的“。这个”一小段代码“就是我们经常说的“单元”。“单元测试”的本来意思也源自于此。比如,你写了个快排函数qsort,想验证下各种序列输入进去是不是就能得到排好序的结果。因为快排的逻辑稍微有一丢丢绕脑,觉得一遍写下来没有信心。于是就用N多的case去验证这个函数的输出是否总是符合预期。
  这样的测试是最简单的,因为要测的东西几乎没有复杂的依赖。这也是大家经常说的“纯函数”好测的原因——函数的输入就是其所有可见的上下文了。对于一个纯函数,开发者者很容易构造其上下文。
  很多文章介绍测试都只拿这种“纯函数”的测试举例子,因此大家看过之后就照猫画虎到实际项目写测试代码了,结果就是很难写出来,或者写出来起不到验证的作用,极度打击自信心,然后慢慢产生了“测试没啥用”或者“测试就应该只让测试工程师负责的“不正确的三观。
  我们正常写的代码可能是业务逻辑,是数据分析的job代码,也有可能是前端构造界面的代码等。这种代码都会有依赖的。由于mock工具的存在,在做“单元测试”的时候,应该尽量保证除待验证的代码之外,所有的不纯的依赖都要尽量mock。
  有些依赖特别麻烦。比如一个例子是,验证一个下单打折的函数是否能计算出正确的折扣。这个函数需要读取数据库里的折扣数据才能做计算。这就引入一个依赖。而对依赖的处理方式的拿捏才是测试里最难的地方。
  比如,你可以直接mock掉整个DiscountDAO。这样就可以避免真的读取数据库。这时,你的假设是“你觉得读取折扣的SQL本身是肯定没错的,因此你不验证它,你只验证读到折扣数据后,根据输入金额得到折扣金额的逻辑是正确的“。
  你也可以用embedded数据库, h2,maria4j之类的mock数据库。但如果你生产用的是mysql/postgres等,那么你在假设“对于这一段要验证的SQL,用embedded db和用生产db是等价的“。这有可能成立,有可能不成立。但也许代码里用到了私有SQL语法,只有生产数据库支持。如果发生,这样的验证就做不了。
  你还可以用真的生产DB做验证。但首先,这里测试的目标已经变了,从“验证计算折扣的逻辑”变成“验证计算折扣这个功能是否正确“。这是已经不是“单元”了,而是一个函数 + 一句SQL执行 + DB功能正确的“集成”测试当然,这个例子里是个很局部的集成。不过也是几个组件一起测试才能实现这个验证。这样的测试代价是必须部署一个真的数据库,还要准备数据和后门。编写代价相对就比较高了。
  开发也可能做(局部)“集成测试”,所以这就是另外一个我觉得应该叫“开发阶段自动化测试“,而不仅仅是“单元测试”的原因。“单元测试“容易让人引起一个误解——开发人员只应该管一小段代码是否正确,却不用管任何集成测试。这明显是不一定正确的。比起“单元测试”这个概念本身,我更在意的是开发人员为了保证代码质量应该怎样做才对。
  那么到底要不要做这种“局部集成测试呢“。这要看情况。比如:
  你的目标就是要测“一小段代码是不是正确”。你可以很有信心的保证其他依赖的正确性都能保证。那明显,这时就不用花精力做集成了。怎么简单怎么来。这样的测试甚至都不需要启动Spring这类框架,运行速度会很快。
  你在开发一个小的lib。这个lib就很纯,没有任何复杂的依赖,那么单元测试就足够了。
  如果团队已经安排了专人做这块的集成测试,开发人员就没必要做重复劳动了。如果这块测试的不好,应该优先去和那个测试同学沟通,看看怎么改善。沟通无效,在manager知情和同意下,再自己补。
  如果开发自己做集成可以更容易构造全集成测试不太容易构造的例子,那么还是自己集成测试一下比较好。性价比高。
  如果是要测试一个端到端的接口返回正确,那么唯一的办法就是集成测试——真的启动server,使用真的数据库、Redis、队列……,做端到端的测试。这时也许docker可以帮助你一键启动全套环境。
  如果一个测试涉及到依赖的核心功能,也必须得做集成测试。比如要测试一个Exception是不是会让当前事务真的回滚,同时发生的其他事务因为隔离级别不会受到影响,那么你必须引入真的,和生产一模一样的支持事务的数据库才行。
  如果是前端测试,基本上也必须得做集成测试。就算可以mock掉所有的后端接口,也得引入浏览器或者App框架才能测试。
  如果整个公司没有专门的测试岗位,都是“研发”,那么不管局部集成还是全部集成,都得由开发者自己上。
  ……
  所以开发的同学在写测试的之前,最好先再三确认自己到底要验证什么。至于用什么工具,如何看待覆盖率,如何设计模块和函数,以及后门,让程序“容易测”都是在目标之下的要讨论的具体问题。
  还有个问题是什么时候应该写测试。是不是项目一开始就得同时写测试,甚至是TDD?
  Well,这也是个Case by Case的问题。单元测试也好,局部集成测试也好,都是有代价的。我的经验是,有效的开发阶段测试会占用掉开发者30%左右的精力。当然,追求技术极致的人会觉得写测试天经地义,不写才是垃圾。30%的精力也是开发的一部分,不带讨价还价的。如果覆盖率不到90%+就是不称职等等。
  我觉得不应该走极端,应该同时看到测试的成本和收益。比如在创业公司早期,就1~2个人的时候,让业务赶紧上线试错找对方向是最重要的。今天的设计和代码说不定几周后就大变样,甚至就直接丢掉了。此时功能简陋,用户量也少,小问题是可以忍的。此时团队会通过手工测试和实际使用来不断的发现和修复问题(所以这时候运行时错误log抓取 + 报警一定要做好啊)。这时强调必须完美覆盖的测试是不合时宜的。这个阶段,应该着重全集成测试(通常由一个专门的测试工程师主持),以及把那些局部的“一旦出问题公司就完蛋”的逻辑加好自动化测试。
  写一个新东西,有的时候一开始想不清楚设计,类、接口、模块的划分可能都不是很完美,这时写一大堆测试基本上是会废掉的。随着不断的迭代,代码的组织才能慢慢优化和清晰,这些稳固的代码产生的接口、模型才有被测试保护的意义。
  但是反过来,如果一开始图快,不写测试,随着业务慢慢稳定,团队人数和用户量慢慢多起来后,习惯性一直不写测试也是不对的。这时对代码的修改会陷入巨大的痛苦和不安中。天晓得改了一处会不会引发其他不相干代码的问题。这时,任何被心灵摧残的开发也会主动想着怎么提高代码质量的。
  因此,需要找一个时间点开始,一点一点的把最重要的、稳固的逻辑的测试补充上。我的经验是一个项目代码在不断迭代大概10个月后,或者团队技术人员已经超过5个人,或者活跃用户已经超过1万人,就必须有测试保护。如果用动态语言(js,python等)编写程序,因为没有编译器的保护,就更要写测试。
  但是最后要特别强调下,搭建有效的自动化测试的工作量并不小。一提起测试,很多人就只想到TestCase。但实际上为了让TestCase发挥作用,让测试容易运行,可以隔离,可重复,稳定的跑,需要学习和实施的东西非常多,甚至不亚于功能开发本身。对测试工作量的低估最终会导致让这个工作不了了之,变成面子工程。如果你是一个技术lead,想带领团队写测试,但以为“就是很小的工作量,随便写一下就会有效果”,那么必然会吃瘪。
  最后,我个人对【开发阶段自动化测试】的要求总结下来是:
  · 确定测试的目标,到底想验证什么
  · 基于这个目标,找到和维护需要的工具,比如Runner,Mock,覆盖率统计工具等,Embedded数据库等
  · 留足给测试的时间,并通过code review的手段来保证写有效的测试
  · 给一些典型的场景如何做测试写一写文档,积累经验(比如如何测试要模拟时间的案例?)
  · 统计测试同学给开发同学报bug的数据,盯紧代码质量不高的同学,多做沟通
  · 根据出现bug的数量和scope来推动部分关键代码的测试质量的改善
  · 在能达成测试目标的前提下,看看能够整合一些工具,降低维护测试依赖的成本
  以及我希望大家写的时候有一个预期:
  【开发阶段自动化测试】是所有测试体系里的一个部分,而非全部,不能解决所有问题。不要觉得写自动化测试的Case,让覆盖率100%就能保证系统绝对不出错。这是妄想,死了这条心。

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号