[译]5步法让TDD变得简单

在测试驱动开发或者单元测试中最困难的事情是什么?当然是编写测试用例啦!
语法或者工具并不是问题——你可以在15分钟内掌握足够的知识,然后开始学习。
问题是如何在你的头脑中获得一些关于你想要做什么的模糊的想法,然后把它变成某种验证某个函数是否正常工作的东西…
人们告诉你使用 TDD。但是你怎么可能为不存在的东西编写测试呢? 我甚至还不知道这个函数要做什么,或者我是否真的想要两个函数而不是一个,而你却想让我为它想一个测试?你疯了吗?
那些告诉你使用 TDD 的人是怎么做到的呢?
这就是问题所在——测试驱动开发需要用不同的方式来思考你的代码。没有人告诉你怎么做。

如何根据一个模糊的想法实现提前写好测试用例

让我带你看看一个你可以用来做这件事的系统——把你想做的任何一种想法,转化成一些有形的、可测试的东西。
我经常使用测试驱动开发。这正是基于我为我的代码编写测试时所经历的思维过程。
这与技术细节无关,比如“red-green-refactor”。相反,我想把重点放在经验丰富的开发人员使用 TDD 编写代码时的思考过程上,这使得编写代码变得很容易。
记住: 你所需要做的就是调整你的思维。就像编写循环和条件一样,编写测试变成了一件你不需要考虑很多的事情。
让我们从第一个例子开始: 计算密码强度。
在我们进入这个过程之前,有一件重要的事情要知道。完美不是目标!测试驱动开发是一个迭代的过程,意味着你要一步一步地重复。是的,我们想做一个有根据的猜测,但是不一定要完全正确。不要总是想着一些小细节,因为在软件中,事情总是会变的。Tdd 最大的优点之一就是它使更改变得更容易,所以如果我们不能在第一次尝试中100% 正确地做到这一点,我们还会再做一次。 这就是它应该有的样子!

步骤1: 确定输入和输出

我们从一个高层次开始这个过程。我们现在还不关心具体实现。
我们的目标是:计算密码强度。为了达到这个目标,我们通常需要一些输入… … 然后,我们得到一些基于它们的输出。
当您通常开始编写代码以达到某个目标时,您可能会从一个函数开始。您可能会考虑函数需要哪些数据,以及它将返回什么样的结果。我们完全可以这样开始这个过程——我们只是不会为它编写任何代码。

  • 输入很简单: 必须是密码
  • 输出也很简单: 它必须是描述密码强度的值。为了简单起见,我们假设密码是强密码或者不是强密码,这样我们就可以为输出使用一个布尔值

    步骤2: 确定函数签名

    现在我们知道了什么数据输入什么数据输出,我们需要选择函数签名——也就是说,函数需要什么参数,以及它是否返回什么。
    这一步同样类似于在没有 TDD 的情况下编写代码的方法。在为函数编写任何代码之前,需要确定函数的参数和返回值。
    首先,确定输入参数。我们的功能需要什么数据?在这种情况下,它很简单——它所需要的只是密码。我们可以完全基于这个值进行整个计算,而且仅仅基于这个值。
    返回值呢?很简单,因为这是一个计算,所以我们可以直接返回结果。在一些更复杂的情况下,返回值可能是一个假设。 或者,该函数可以接受一个回调参数,而不是返回一个值,或者它可能根本不返回任何东西。
    不管怎样,现在我们可以决定在代码中调用这个函数是什么样的:
    1
    var strong = isStrongPassword('password string goes here');

    第三步: 实现函数细节

    我们现在知道了目标、涉及的数据和函数签名。
    在非 tdd 工作流中,您现在就可以开始编写函数的代码。 、您可能已经有了一些关于如何工作的想法——我们需要检查这个,我们需要检查那个,返回值受 x 影响。
    这就是大多数人在 TDD 上遇到麻烦的地方。你的头脑中充满了关于如何编写函数的想法… 但是你不确定如何布置代码,直到你开始编写它。
    与其考虑所有的情况… … 不然让我们先专注于一件小事。
    为了更接近我们的目标,我们需要做的最简单的行为是什么?
    一个常见的问题是试图解决一大堆真正重要的行为。如果我们考虑密码强度,有不同的规则,如特殊字符,数字,密码长度,等等的想法。当然,很难想象一个测试能涵盖所有这些内容!
    那么,为了让这个函数更接近验证密码的最终目标,我们可以采取哪些最简单的步骤呢?
    如果在没有 TDD 的情况下构建这个函数,您将编写的第一行(或两行)代码是什么?
    为了让函数更接近工作状态,我们可以添加的最小代码量是多少?
    密码强度最简单的规则可能是空密码。这真的很容易——当密码为空时,输出应该总是 false。

    第四步: 实施测试

    就这样,我们开始编写测试用例。 我希望这比你想象的要简单:)
    请注意,前面的所有步骤实际上与编写没有 TDD 的代码是多么相似?
    主要的区别在于,我们没有关注函数的实现,而是关注如何调用函数,以及结果是什么。也就是说,我们在考虑函数在某些条件下的行为。
    函数的行为是我们想要测试的。一旦您开始在某些条件下测试行为(比如某些参数、一天中的某个时间等等),测试就会变得容易得多,因为我们可以从外部观察行为。如果我们只是选择行为,我们不需要知道实现。
    我们决定将密码作为函数的唯一参数。我们还决定返回一个布尔值来指示密码是否强。
    我们还选择了对于空密码,结果应该总是false-表明一个空密码是弱。
    让我们把所有这些代入一个测试:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    describe('isPasswordStrong', function() {
    it('should give negative result for empty string', function() {
    var password = '';

    var result = isPasswordStrong(password);

    expect(result).to.be.false;
    });
    });
    请注意,在不知道函数中代码的确切行数的情况下,我们很容易就编写了这个函数。我们决定给定一个空字符串作为参数,结果应该是 false。 一个简单的行为,很容易转化为一个测试。

    第五步: 实现函数代码

    我们只需要添加使测试通过的最小量的代码。
    1
    2
    3
    4
    5
    function isPasswordStrong(password) {
    if(!password) {
    return false;
    }
    }
    如果我们继续开发密码强度函数,我们所要做的就是重复这个过程。我们回到第三步,选择下一小步。第四步,添加测试。 第五步,执行。重复这些操作。
    如果您一直像这样小步前进,TDD突然变得容易得多。是的——您可能需要对相当少量的代码进行几次测试,但这并不是一件坏事。Tdd 通过这种方式帮助您减少可能编写的无用代码的数量,因为您添加的每一行代码都经过测试验证。

一个更详细的例子

这套流程真的能解决更复杂的问题吗?看起来很简单,不是吗。
剧透警告: 答案是肯定的,它可以!
让我们来看一个稍微复杂一点的例子,这样您就可以更直观到这套流程将如何应用于具有更多灵活部件的东西。
debounce防抖函数怎么样?防抖函数:如果已经在一定时间内调用了某个函数,那么它将确保在这段时间内不会调用其他函数。例如,如果您需要处理滚动事件,这就很方便,因为通常只希望在用户停止滚动后触发事件处理。
由于这涉及到时间,它更具挑战性,我制定了5个步骤来实现。
让我们再从第一步开始。 防抖函数的输入和输出是什么?
因为我们的目标是创建一个现有函数的版本,除非经过一段时间,否则它不会被调用,所以第一个输入应该是一个函数。 第二个输入可以是我们想要拆除它的时间量。
作为输出,拆分函数需要返回原始函数的延迟版本,以便可以调用它。
步骤2: 函数签名。我们将这两个输入作为参数传递给函数,它将返回一个新函数。很简单。
所以我们得到了这样的结果:

1
var delayedFunction = debounce(targetFunction, delayInMilliseconds);

现在,更有趣的部分… 在第三步中,我们需要选择函数的一小部分来实现。函数有一个延迟,如果我们多次调用返回的函数,它不应该被调用。除非我们有足够长。
但是让我们从最简单的事情开始。如果我们调用debounce返回的延迟函数,它应该等待一段时间,然后运行原始函数。我认为这似乎是一个合适的起点。
我们已经进入了第四步,这个测试会是什么样的呢?
和以前一样,让我们先把我们选择的代码输入到测试中:

1
2
3
4
5
6
7
8
9
10
11
12
describe('debounce', function() {
it('should call returned function after delay passes', function(done) {
var delay = 5;
var targetFn = function() {
done();
};

var delayedFn = debounce(targetFn, delay);

delayedFn();
});
});

我们知道我们需要以毫秒为单位的延迟,所以我们从这个开始。 我们还需要目标函数。因为我们知道在延迟之后需要调用目标函数,所以我们可以通过在目标函数中调用 done callback 来快速简单地验证测试通过。没有调用 done-test 失败。
接下来,我们称之为debounce。正如我们前面选择的那样,我们传入这两个参数并获取输出。最后,我们调用输出来测试这种行为: 在调用延迟函数和延迟之后,应该调用目标函数。
我们不需要知道具体的实现——我们只需将前面步骤中的信息直接插入到测试中。我们唯一需要知道的是,在 JavaScript 中,延迟是异步的,因此我们需要一个异步测试。 是的,这可能是一个实现细节,但是当你知道 JavaScript 是如何工作的,并且不是特定于这个特定的函数的时候,它就自然而然地出现了。
我们可以继续第5步,现在就实现代码:

1
2
3
4
5
function debounce(targetFn, delay) {
return function() {
targetFn();
};
}

等一下! 这根本不是延迟功能!
是的-我们正在做 TDD!可以说,我们所需要做的就是满足我们编写的测试… 这段代码使测试通过。
有人可能会说这有点欺骗,毕竟我们知道这不是正确的行为。 Tdd 的一种解释只要求实现足够的代码来使测试通过,所以让我们继续。
我们将回到第三步,选择另一个微小的行为来实现。 一个非常重要的行为就是不要像我们现在的代码那样过早地调用函数。
好了,这是我们向前迈出的第二小步。第四步,实现测试:

1
2
3
4
5
6
7
8
9
10
it('should not run debounced function too early', function() {
var delay = 100;
var targetFn = function() { };

var delayedFn = debounce(targetFn, delay);

delayedFn();

//but how do we verify it now?
});

我们需要Sinon’s fake timers。 我们可以使用它们来创建一个假的计时器,然后将它向前推进,然后确保延迟函数的调用不会比预期的提前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
it('should not run debounced function too early', function() {
var clock = sinon.useFakeTimers();

var delay = 100;
var targetFn = sinon.spy();

var delayedFn = debounce(targetFn, delay);

delayedFn();
clock.tick(delay - 1);

clock.restore();
sinon.assert.notCalled(targetFn);
});

首先,我们启用了Sinon’s fake timers。 注意,我将目标函数更改为一个 Sinon spy。 这使我们以后可以很容易地验证函数是否被调用了。
在调用 delayedFn 之后,我们使用 clock.tick 来提前时间。但是,我们只比延迟所需的时间提前1毫秒。 这样,我们调用 sinon.assert.notCalled,我们可以确保目标函数不会过早地被触发。
如果你想了解更多关于 Sinon 的功能或者假计时器的信息,你可以在这里找到具体信息,因为它会更详细地介绍这个功能。

总结

从示例中可以看到,我们可以对所有类型的函数应用相同的五个步骤。
如果你正在寻找一些实践,你可以使用我们在这里开始实现的两个函数中的任何一个,看看你是否可以应用这5个步骤来使这些函数完全发挥作用。
一旦你掌握了基本知识,测试驱动开发就不难了。挑战在于它需要你反复思考: 没有 TDD,你直接思考如何实现某些东西。 但是使用 TDD 时,您需要考虑希望某些东西的行为如何。

  1. 函数的输入是什么,调用函数的输出(行为)是什么?
  2. 决定如何从代码中调用函数
  3. 为一些你能想到的输入选择最小可能的行为片段
  4. 编写一个测试,使用这些输入调用函数,并验证行为
  5. 实现足够的代码使测试通过
    如果我们遵循这些简单的步骤,预先编写测试就会变得容易得多。当您继续处理代码时,您可以在步骤3到5之间重复。
    记住——如果您实现了一些测试和代码,但后来发现它必须以不同的方式工作,那没关系!继续前进,重做它-我们不需要完美的第一次尝试,追求它只会让你陷入困境。这不仅仅是 TDD 的问题: 无论如何,您可能需要重做和重构代码的某些部分。TDD 只是使其更加安全,因为您有测试来验证代码不会因为更改而出错。

原文:5 step method to make test-driven development and unit testing easy