不要把逻辑放在测试用例中!

乔梁 | 2021-02-06

**编程语言给了我们很大的表达能力。**运算符和条件表达式等概念是重要的工具,让我们能够编写程序,处理各种各样的输入。但是,这种灵活性是以增加复杂性为代价的,它们令我们的程序更难理解。

与生产代码不同,测试中的简单性比灵活性更重要。大多数单元测试都验证一个已知的输入是否产生一个已知的输出。测试应该通过直接声明它们的输入和输出,而不是通过计算来得到,从而避免复杂性。否则,测试用例本身主很容易出现测试自己的 bug 。

让我们看一下下面这个简单的例子。一眼看上去,这个测试是正确的,对么?

@Test public void shouldNavigateToPhotosPage() {
  String baseUrl = "http://plus.google.com/";
  Navigator nav = new Navigator(baseUrl);
  nav.goToPhotosPage();
  assertEquals(baseUrl + "/u/0/photos", nav.getCurrentUrl());
}

作者试图通过在变量中存储共享前缀来避免重复。执行单个字符串的拼接,看起来并不太糟糕,但是如果我们此时做“变量内联”的操作,来简化这个测试用例,会怎么样?

@Test public void shouldNavigateToPhotosPage() {
  Navigator nav = new Navigator("http://plus.google.com/");
  nav.goToPhotosPage();
  assertEquals("http://plus.google.com//u/0/photos", nav.getCurrentUrl()); // Oops!
}

在从测试用例中消除了那些不必要的计算之后,错误就显而易见了—— 我们期望 URL 中有两个斜杠!如果生产代码有相同的 bug ,这个测试用例要么失败,要么(甚至更糟)被错误地通过了。

假如,我们并不试图来拼接它们,而是直接表述我们的输入和输出,那么就不会有这个问题存在,本文也不必存在了。

这是一个非常简单的例子-当一个测试用例包含了更更多的操作符运算,或者包含了循环和条件表达式时,就越来越难以确信它是正确的了。

换一种说法,就是,虽然生产代码描述的是“给定输入,计算其输出”的一般策略,但是,测试用例并不是一般策略,而是对输入/输出的一个具体示例,是一个实例化的需求(其中,它的输出可能包括验证与其他类的交互等副作用)。通常,即使计算它所需的通用逻辑非常复杂,我们也应该很容易判断“输入”与“输入”这对搭档是否一一匹配且正确。

例如,我们很难确切地描绘一个 Javascript 函数为给定服务器响应所创建的整个 DOM 。因此,对这样一个函数进行理想的测试,只需与包含预期输出 HTML 的字符串进行比较,就可以了。

当测试用例确实需要它自己的计算逻辑时,那么,你应该确保这种逻辑从测试体中移出,可以放入一个 Helper 函数中,或是一个单独的帮助文件中。由于这种辅助程序可能会变得非常复杂,所以对于这种非寻常的测试辅助程序来说,拥有自己的测试通常是一个好主意。


发表时间:July 31, 2014

原文作者:Erik Kuefler

原文链接Testing on the Toilet: Don’t Put Logic in Tests