漫谈单元测试

前言

提起单元测试,有人觉得它没什么用,纯属浪费时间;有人则一头雾水;当然也有人认为单元测试很重要,无论对项目的开发还是对程序员自身的提升都大有益处。

拿自身经历来讲,之前呆过的一个小团队也曾推过这个东西,但没过一段时间就不了了之;作为当事人,我当时的感受就是完全不明白这东西是干什么的,一头雾水,觉得怎么写完程序还要写这东西,浪费时间。但最近在尝试参与 chromium 项目时,发现单元测试是一门必须课;而且对于很多开源项目,单元测试也都是不可获取的一部分。单元测试真的没用吗?是时候静下心来去思考一下了。

从使用的角度来讲,单元测试并不复杂。但我觉得对于单元测试的学习而言,学会使用只是最基础的,只有搞清楚了为什么要用、单元测试能帮我们解决哪些问题、单元测试又有那些局限这些问题,才能真正明白单元测试的真谛,真正的用好单元测试;所以,在这篇博客中,我不会讲某个单元测试框架,也不会去讲单元测试的概念,而是结合自己以前的一些工作经历去思考,为什么我们需要单元测试?单元测试能帮我们解决那些问题,又有那些局限,单元测试的本质。

为什么要使用单元测试

单元测试的存在不是为了给程序员增加负担,而是为了解决在软件开发中普遍存在的一些问题。如今,当初创造单元测试的大师们是怎么意识到这些问题并提出了单元测试这个解决方案我们不得而知;但我们可以自己去思考在自己的项目经历中存在的一些问题。

软件开发中的困境 - 无休无止的测试

你们的项目在测试和修复 Bug 上花费了多少时间?

13 年刚进公司时我曾参与一个远控项目的开发,该项目从 8 月份处开始,到 12 月初结束,历时 4 个月,而这其中花费在测试和修复 Bug 上时间就有 2 个月,当时,作为开发人员我在这两个月内的节奏基本是这样的:

  1. 被测试人员测出一个 BUG
  2. 修复 BUG
  3. 自己把出现该 Bug 的功能测一下;这里除了要测试引起该功能出现这个 Bug 的场景外,往往还需要对该功能的其他使用场景进行测试,甚至还要对这个 Bug 可能影响到的其他功能进行测试
  4. 测试成功后提交 Bug 管理系统,等待测试人员进行回测后确认 BUG 是否已解决。
  5. 测试人员进行回测,以确认该 BUG 是否已修复。同样,这个阶段测试人员除了要测试引起该功能出现这个 Bug 的场景外,往往也需要对该功能的其他使用情景测试,甚至测试这个 Bug 可能影响到的其他功能

上面就是当时整个团队在那段时间的节奏,我想很多人应该都有类似经历。

那这个过程麻烦吗?在回答这个问题之前,我们不妨先问问自己,上面这个过程在自己的项目经历中重复了多少次?而自己每次又在第 4 步花费了多少时间?测试人员又在第 5 步花费了多少时间?

可以说,上面这个过程不麻烦;但当这个过程反复出现时,就麻烦了;另外如果在第 4 步和第 5 步中开发人员和测试人员只是测试引起功能出现 Bug 的使用场景,那还好;但实际情况是,开发人员和测试人员往往要测试出现 Bug 的功能的多种使用情景,甚至再测试可能受该 Bug 影响的其他功能。当上面这些情况放在一起时,你会发现花费在测试上的时间在以难以想象的速度在增长。

怎么办?首先,规范测试方法和测试流程是一方面,但怎样让这些测试自动化才是关键,因为无论测试方法和测试流程有多规范,但只要这些测试在重复(这无法避免)且需要手动进行,那团队花费在测试上时间就不会减少。而单元测试就是为了测试的自动化而生,相对于手动进行测试,单元测试难以想象的快,而且可以反复进行。

软件开发中的困境 - 你敢重构吗

当接手了一个老项目,觉得代码很烂,想进行重构或者重写部分代码,但是你真的敢吗?

前段时间修复一个老项目中存在的一些 Bug 时,当时觉得代码质量实在不高,就想对一些代码进行重写(注意,仅仅是重写一些代码),但一想到重写这些代码可能付出的代价后,还是放弃了,转而小心翼翼的在原有代码的基础上去做些小的修改。为什么?因为这不是简单的重写部分代码的问题,而是在这部分代码重写后,可能相关的测试都要来重新来一边的问题;而且一旦在这个过程中出现问题,那所有的责任都是我的;因为至少以前这段代码在大部分情况下是没问题的;另外项目组老大估计也不会同意。为什么?我觉得其实他也怕。

但是假如这个项目有之前积累的非常完善的单元测试呢。我想情况会稍有不同。因为在重写后,相关的测试可以很快完成,即使出错,通过单元测试框架提供的信息,错误也能很快定位到,立刻得到解决。

这便是单元测试的另外一个好处。当项目有非常完善的单元测试时,测试也就不在是一件耗费时间且无聊的事,而我们在修改或重构现有代码时也能放下包袱,大胆尝试。

单元测试的能与不能

上面说的很美好,但单元测试真的能在测试方面彻底解放我们吗?下面让我们结合实际来讨论下这个问题。

首先我们来介绍一个单元测试框架 googletest。googletest 是由 Google 开发的开源 C++ 单元测试框架。也许你之前没有接触过 googletest,甚至从来没有接触过单元测试。没关系,这里不需要纠结什么概念;往下看,你会发现,单元测试的思想很简单,googletest 的核心东西也很简单。

简单来讲,googletest 其实就是给我们提供了一系列的判断真假的宏,如 ASSERT_EQ(expected, actual) 用于判断 expected 和 actual 是否相等;ASSERT_GT(val1,val2) 用于判断 val1 是否大于 val2;ASSERT_STREQ(expected_str, actual_str) 用于判断 expected_str 和 actual_str 这两个字符串是否相等。这些宏就是 googletest 的核心,直接决定着 googletest 能用来做什么;至于 googletest 提供的如 Test Fixtures 之类的特性,只是它为我们提供的在使用这些宏时遇到的一些常见需求(如,不同 Test 之间共享资源)的通用解决方案,锦上添花而已。

而为项目写单元测试,其实主要就是用这些宏去测试程序中的函数是否正确。我们通过指定的参数去调用程序中的函数,然后用这些宏比较函数的实际返回值与正确的返回值是否相等;如果相等,说明函数就是正确的;如果不相等,就说明函数就 bug。

比如我们的项目中有一个下面一个函数:

// Returns n! (the factorial of n).  For negative n, n! is defined to be 1.
int Factorial(int n) {
  int result = 1;
  for (int i = 1; i <= n; i++) {
    result *= i;
  }

  return result;
}

那我们为这个函数所写的单元测试大致就是下面这样的:

EXPECT_EQ(1, Factorial(-5));
EXPECT_EQ(1, Factorial(-1));
EXPECT_GT(Factorial(-10), 0);

通过用不同的参数去调用 Factorial 然后比较它的返回值和正确的返回值,来测试 Factorial 在不同情况是否都能正确执行。当 Factorial 通过这些测试时,基本就可以说 Factorial 肯定是没问题的。

这便是单元测试。很简单不是吗!但看到这里,我想此时此刻肯定会有很多人会疑问,这真的有用吗?我觉得,在给自己一个答案之前,先不妨想一下自己项目组的 bug 列表里,有多少 bug 是因为函数自身的实现就存在问题而导致的,而定位和解决这些 bug 又花费了多长时间。下面讲讲我的理解。

软件由功能组成;而功能主要由函数组成。那如果能保证组成一个功能的所有函数都是正确的,能不能说这个功能在 90% 的情况下是没有 Bug 的呢;进一步讲,如果这个软件的所有函数都通过了单元测试,那能不能说这个软件在 90% 的情况下是没有 Bug 的。这就是单元测试的思想,通过测试每一个函数的正确性,来保证软件功能的正确性,来保证整个软件的正确性。

的确,单元测试的核心很简单,就是一些判断真假的宏;但重要的不是这些宏,而是这种 1 + 1 > 2 的思想和单元测试的可重复性。

当然,不得不说单元测试的也是局限的,单元测试的实现方式注定着它无法检测程序中的一些不仅仅是保证代码正确就能避免的 bug,如由程序自身的设计缺陷而引起的 bug。记得在之前的一个远控项目中,当时程序在内网进行测试时没有任何问题,但一旦拿到公网下,在客户端需要往管理器返回大量数据时,就会出现异常;而出现该问题的本质原因就是客户端的设计存在缺陷;当时客户端发送数据时使用异步操作,而且在发送数据时不会等待上次发送操作结束后在发送,而是立即发送,这样在数据量非常大的情况下,就会导致存储未发送的异步数据的空间用完,从而导致程序抛出异常。就这个 bug 而言,通过单元测试肯定是检测不出来的。

但是,单元测试依旧为我们解决了绝大部分的问题,不是吗?

More

一个工具再好,不会用,也能被用烂了。单元测试就是这样,这篇博客也只是讲了自己的一些想法而已,有兴趣的同学可以更深入学习,这里强烈推荐《你好,单元测试!》这篇文章。

另外,其实我们不仅仅是在思考单元测试,而是在思考软件开发,思考软件开发过程中的存在的常见问题和这些问题的通用解决方案,思考怎么改善这个这个过程。我们是程序员,我们写代码,但我们也不仅仅写代码,我们也要尝试从一个更高的角度看问题。

你可能感兴趣的:(胡思乱想)