从可维护性角度分析怎么编写优秀的单元测试(上)

发表于:2022-1-18 09:43

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

 作者:echolc55873    来源:稀土掘金

  可维护性是大多数开发者在编写单元测试时面对的最核心的问题之一,最终,随着项目的发展,测试可能会变得越来越难维护和理解,系统的每一个改变,即使没有缺陷,也可能导致测试失败。本文将从其它以下几个大的方面介绍怎么编写易维护的测试:
  · 只测试公共方法;
  · 删除重复的测试代码;
  · 实施测试隔离。
  当然这几个大的方面会包含很多小的测试技术介绍。
  测试私有或受保护的方法
  开发人员把方法设为私有方法或者受保护的,通常有一些理由,有时是为了隐藏细节,以便将来实现的变化不会影响外部功能,还有可能是出于安全性或者知识产权相关的原因考虑。
  测试私有方法,你测试的是系统内部的一个契约,这个契约可能会变化,导致你的测试不可靠。而且私有方法既然被创建,它肯定会被其它公共方法调用,所以在测试其它公共 API 的时候,它会作为系统的一部分一起执行。所以当你测试私有 API 的时候,你应该找到调用私有方法的公有API,并针对这个 API 进行测试。如果一个 API 有需要单独测试的必要,那它应该被设置成公有 API。当然这不是说基础代码中不应该包含私有方法,使用 TDD 开发,通常会对公共方法编写测试,这些公共方法会被重构调用较小的私有方法,在此过程中,对公共的方法测试始终能通过,私有方法也得到了测试。
  要提高测试可维护性,另一个方法是去除测试中的重复代码。
  去除重复代码
  单元测试中的重复代码和产品代码中的重复一样有害,带来维护性问题。DRY原则也适用于测试代码,重复代码意味当需要修改代码的时候需要重复性地修改多个地方,当测试类的函数变更或者使用类的语义变化就会对测试产生很大的影响。下面我们看一个例子:
  class LogAnalyzer {
  isValid (fileName: string) {
    if (fileName.length < 8) {
      return true
      }
      return false
    }
  }
 
  该类对应的测试:
  test('LogAnalyzer isValid invalid fileName', () => {
    const logan = new LogAnalyzer()
    const res = logan.isValid('12345')
    expect(res).toBeFalsy()
  })
  test('LogAnalyzer isValid valid fileName', () => {
    const logan = new LogAnalyzer()
    const res = logan.isValid('12345789')
    expect(res).toBeTruthy()
  })

  可能大家都觉得上面的测试没有什么问题,但是如果 LogAnalyzer 类的使用语义发生了变化,需要在使用任何 API 之前调用 init 方法,那么所有的测试都要修改。修改 LogAnalyzer实现:
  class LogAnalyzer {
    private initialized = false;
    
    init () {
    // 其它初始化逻辑
      this.initialized = true
    }
    
  isValid (fileName: string) {
      if (!this.initialized) {
      throw new Error('LogAnalyzer must be initialized')
      }
      
    if (fileName.length < 8) {
      return true
      }
      return false
    }
  }

  在改造 LogAnalyzer 后,上面的两个测试都会失败。所以,我们需要重构上面例子中的测试,下面介绍几种重构的方式:
  1.使用辅助方法去除重复代码,在上面的例子中,我们可以将 LoganAnalyzer 创建的逻辑,使用一个工厂方法封装起来:
  function createLogan () {
    const logan = new LogAnalyzer()
    logan.init()
    return logan
  }
  test('LogAnalyzer isValid invalid fileName', () => {
    const logan = createLogan()
    const res = logan.isValid('12345')
    expect(res).toBeFalsy()
  })

  这样,我们将可能变化的部分单独隔离起来,使得测试的维护性提高。
  2.使用 setup 方法去除重复代码
  在单元测试框架中,一般都会提供可以让你初始化一些代码逻辑的钩子,比如在 Jest 中,就提供了 beforeEach 、 beforeAll 等,你可以通过这些钩子去消除重复的代码:
  let logan: LogAnalyzer
  beforeEach(() => {
    logan = new LogAnalyzer()
    logan.init()
  })
  test('LogAnalyzer isValid invalid fileName', () => {
    const res = logan.isValid('12345')
    expect(res).toBeFalsy()
  })
  test('LogAnalyzer isValid valid fileName', () => {
    const res = logan.isValid('12345789')
    expect(res).toBeTruthy()
  })

  这种方式,就不需要在每个测试中单独写创建 logan 的逻辑。当然过度使用 setup 也不一定好,下面就要提到怎么编写可维护的 setup 方法。

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

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号