51Testing软件测试论坛

标题: 从可维护性角度分析怎么编写优秀的单元测试(上) [打印本页]

作者: lsekfe    时间: 2022-1-24 13:19
标题: 从可维护性角度分析怎么编写优秀的单元测试(上)
可维护性是大多数开发者在编写单元测试时面对的最核心的问题之一,最终,随着项目的发展,测试可能会变得越来越难维护和理解,系统的每一个改变,即使没有缺陷,也可能导致测试失败。本文将从其它以下几个大的方面介绍怎么编写易维护的测试:
  · 只测试公共方法;
  · 删除重复的测试代码;
  · 实施测试隔离。
  当然这几个大的方面会包含很多小的测试技术介绍。
  测试私有或受保护的方法
  开发人员把方法设为私有方法或者受保护的,通常有一些理由,有时是为了隐藏细节,以便将来实现的变化不会影响外部功能,还有可能是出于安全性或者知识产权相关的原因考虑。
  测试私有方法,你测试的是系统内部的一个契约,这个契约可能会变化,导致你的测试不可靠。而且私有方法既然被创建,它肯定会被其它公共方法调用,所以在测试其它公共 API 的时候,它会作为系统的一部分一起执行。所以当你测试私有 API 的时候,你应该找到调用私有方法的公有API,并针对这个 API 进行测试。如果一个 API 有需要单独测试的必要,那它应该被设置成公有 API。当然这不是说基础代码中不应该包含私有方法,使用 TDD 开发,通常会对公共方法编写测试,这些公共方法会被重构调用较小的私有方法,在此过程中,对公共的方法测试始终能通过,私有方法也得到了测试。
  要提高测试可维护性,另一个方法是去除测试中的重复代码。
  去除重复代码
  单元测试中的重复代码和产品代码中的重复一样有害,带来维护性问题。DRY原则也适用于测试代码,重复代码意味当需要修改代码的时候需要重复性地修改多个地方,当测试类的函数变更或者使用类的语义变化就会对测试产生很大的影响。下面我们看一个例子:
  1.  class LogAnalyzer {
  2.   isValid (fileName: string) {
  3.     if (fileName.length < 8) {
  4.       return true
  5.       }
  6.       return false
  7.     }
  8.   }
复制代码
该类对应的测试:
  1. test('LogAnalyzer isValid invalid fileName', () => {
  2.     const logan = new LogAnalyzer()
  3.     const res = logan.isValid('12345')
  4.     expect(res).toBeFalsy()
  5.   })
  6.   test('LogAnalyzer isValid valid fileName', () => {
  7.     const logan = new LogAnalyzer()
  8.     const res = logan.isValid('12345789')
  9.     expect(res).toBeTruthy()
  10.   })
复制代码
可能大家都觉得上面的测试没有什么问题,但是如果 LogAnalyzer 类的使用语义发生了变化,需要在使用任何 API 之前调用 init 方法,那么所有的测试都要修改。修改 LogAnalyzer实现:
  1.  class LogAnalyzer {
  2.     private initialized = false;
  3.     
  4.     init () {
  5.     // 其它初始化逻辑
  6.       this.initialized = true
  7.     }
  8.     
  9.   isValid (fileName: string) {
  10.       if (!this.initialized) {
  11.       throw new Error('LogAnalyzer must be initialized')
  12.       }
  13.      
  14.     if (fileName.length < 8) {
  15.       return true
  16.       }
  17.       return false
  18.     }
  19.   }
复制代码
在改造 LogAnalyzer 后,上面的两个测试都会失败。所以,我们需要重构上面例子中的测试,下面介绍几种重构的方式:
  1.使用辅助方法去除重复代码,在上面的例子中,我们可以将 LoganAnalyzer 创建的逻辑,使用一个工厂方法封装起来:
  1.  function createLogan () {
  2.     const logan = new LogAnalyzer()
  3.     logan.init()
  4.     return logan
  5.   }
  6.   test('LogAnalyzer isValid invalid fileName', () => {
  7.     const logan = createLogan()
  8.     const res = logan.isValid('12345')
  9.     expect(res).toBeFalsy()
  10.   })
复制代码
 这样,我们将可能变化的部分单独隔离起来,使得测试的维护性提高。
  2.使用 setup 方法去除重复代码
  在单元测试框架中,一般都会提供可以让你初始化一些代码逻辑的钩子,比如在 Jest 中,就提供了 beforeEach 、 beforeAll 等,你可以通过这些钩子去消除重复的代码:
  1.  let logan: LogAnalyzer
  2.   beforeEach(() => {
  3.     logan = new LogAnalyzer()
  4.     logan.init()
  5.   })
  6.   test('LogAnalyzer isValid invalid fileName', () => {
  7.     const res = logan.isValid('12345')
  8.     expect(res).toBeFalsy()
  9.   })
  10.   test('LogAnalyzer isValid valid fileName', () => {
  11.     const res = logan.isValid('12345789')
  12.     expect(res).toBeTruthy()
  13.   })
复制代码
这种方式,就不需要在每个测试中单独写创建 logan 的逻辑。当然过度使用 setup 也不一定好,下面就要提到怎么编写可维护的 setup 方法。






欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/) Powered by Discuz! X3.2