大型企业通常如何进行单元测试?
你平时是怎么做单元测试的?
面试官心理预期
面试官询问单元测试并非仅仅想了解这一概念,背后可能考察面试者以下三个方面:
基于以上三点,我们需要思考什么样的单元测试才能被视为有效?
高手回答
整个软件工程的生命周期大致分为以下阶段:
- 需求分析阶段:包括需求调研、设计和评审
- 设计阶段:主要集中在架构设计
- 开发阶段:正式开始编码工作
- 测试阶段:完成编码后,包括:
自测:单元测试 -> 集成测试
提测:QA介入集成测试,进行多轮测试
- 发布阶段:QA完成测试后,可以进行上线,其中包括:
-
预发布:部署到线上环境,QA进行回归测试,逐步增加流量,观察是否存在异常
-
正式上线:若预发布无问题,则代码正式上线,根据灰度或A/B测试策略控制新功能流量比例,经过稳定运行一段时间无异常后,逐步放开全部流量。
我们再深入分析每个阶段发现缺陷的成本,主要指从发现到解决问题所需的人力时间成本:
- 需求分析阶段:如果设计评审发现不合理,可以选择不执行,仅需花费几个小时进行会议讨论。
- 设计阶段:架构设计也需要评审,同样只需要几个小时会议时间。
- 开发阶段:如果前两个阶段没有问题,小型功能修复通常需要几小时,大型功能可能需要几天甚至更长时间,可能导致开发出无效功能,需要重新设计和开发,带来重复劳动的局面。
- 测试阶段:无论是自测还是提测的集成测试,修复一个缺陷意味着重新部署代码,对于大型项目,启动时间可能是分钟级。不论是自测还是提测,修复多个缺陷会阻塞测试进度,多次部署累计的时间成本非常高。而单元测试一个案例通常只需要毫秒或秒级,做好单元测试可以显著提高效率。许多公司非常重视单元测试的覆盖率和有效性,甚至将单元测试纳入持续集成/持续交付流程,仅当所有单测通过才能部署。同时,QA团队也极为关注阻塞测试进度的情况。
- 发布阶段:通常经过QA严格测试后才进入发布阶段,虽然不会出现明显的缺陷,但也不能排除存在问题。某些缺陷可能在实际用户请求或高流量时才会显现,这些越过测试和预发布环境的问题可能会在线上直接暴露。灰度和A/B测试的部分目的是将线上问题造成的影响最小化。这也解释了即使在各大互联网公司,仍可能发生事故。这种情况不仅涉及时间成本,严重的缺陷可能带来直接的经济损失和用户流失,一旦程序员出现问题,将成为谈资。因此,许多公司非常重视缺陷漏测率,即测试阶段未发现的问题。
上述内容提到了单元测试的关键要点,以下是编写优质单元测试的方法总结:
如何编写单元测试
- 随机事件:例如随机数,最好使用模拟(Mock)进行控制;
- IO操作:无论是磁盘IO还是网络IO(如数据库、外部接口),都需要隔离,最好也进行模拟。
- 传入错误参数的反应;
- 依赖返回不正确结果的情况。
异常情况包括:
- 外部异常:依赖(内部或外部接口、数据库环境等)抛出异常将如何处理;
- 内部异常:代码本身抛出RuntimeException的后果。
另一个优秀的策略是采用测试驱动开发(TDD)方法,即先列出所有可能的测试用例,然后再开始实现逻辑代码。这种方式可以快速创建出完备的单元测试集合。值得注意的是,在国内很少有公司采用TDD开发模式。
领域驱动设计(DDD)强调明确的边界划分,事件风暴和防腐层的设计为测试驱动开发(TDD)和单元测试提供了良好的基础。领域驱动设计(DDD)中倡导清晰的边界划分,通过事件风暴和防腐层设计,为TDD和单元测试提供了有力支持。
前文提到使用Mock对象来隔离I/O操作和随机事件,当然,Mock也可以应用于各种依赖关系,比如Spring Bean之间的依赖、工具类、各种内部接口的依赖等。Mock的作用是模拟所依赖的资源,我们可以假定依赖操作是成功或失败的,这样测试只需关注自身代码对依赖产生的响应结果即可。
Java的单元测试
Java工程也可以集成Spock框架进行单元测试,Spock使用Groovy语言编写测试用例。由于Groovy是一种动态语言,非常灵活,非常适合编写简洁的单元测试代码。同时,Spock不仅局限于模拟(Mock),还提供各种高效的功能(这些是传统JUnit和Mockito无法实现的):
所以编写优秀的单元测试代码是卓越程序员的基本修养。因为针对有用户访问和无用户访问的项目,相同的代码甚至在极端用户流量下可能带来截然不同的效果。在面对极端用户流量时,每次修改一行代码上线都如履薄冰。怀着敬畏之心对待每一次上线和线上操作,至关重要。