1.4 让测试通过

我们刚才写测试的时候,暂时忽略了语法上有可能出现编译错误的所有地方,只想着把自己期望的结果表达出来。这样做合适吗?

在刚刚开始的时候,通过极少量的代码来摆正我们的前进方向确实是合适的,我们目前所处的正是这种刚刚开始的状态。当然,由于还没定义Dollar就开始使用它,因此测试会失败。这时有人可能会来一句:“那还用说?”然而大家目前还是得稍微有一点点耐心才行,因为我们至少做到了这样两点:

1.我们已经完成了第一步,也就是让第一个测试变红(或者说,写出了第一个失败的测试)。对于所要实现的每一个功能来说,编写失败的测试都是实现该功能时的第一步,而我们现在所要实现的是整个程序的第一个功能,因此我们不仅处在这个功能的起点,而且处在整个程序的起点。

2.我们可以(而且乐意)在开发后续功能的时候,逐渐提升实现每个功能的速度。然而我们同时也知道在需要放慢脚步的时候可以慢下来。

RGR环的第二个环节是让测试变绿(也就是令其通过)。

我们显然需要抽象出这个名为Dollar的概念。这一节就定义如何引入此抽象和其他一些必要的抽象,让我们的测试能够通过。

1.4.1 Go

在money_test.go末尾添加一个空白的Dollar结构体:

这次运行测试,我们会看到一条新的错误消息:

不错,有进展了!

这条错误消息指引我们给Dollar结构体添加名为amount的字段。我们现在就做。对于当前的目标来说,只需要用int类型设计这个字段就足够了:

把这个字段添加到Dollar结构体之后,接下来运行测试的时候当然就会遇到这样一条错误消息:

大家可以看到一条规律:如果还没有定义某个东西(例如某个字段或方法)时就使用它,那么Go语言的运行时库会给出undefined错误。以后我们会利用这条规律来提升TDD的速度,从而更快地走完每一个RGR环。现在,我们先添加名叫Times的函数。根据我们所写的测试,这个函数必须接受一个(表示乘数的)数字,并返回另一个(表示相乘结果的)数字。

然而,这个结果应该怎样计算呢?我们当然知道基本的算术规则,也就是说,我们知道怎样用编程手段来计算两数相乘之积。但是,现在只需要用最简单的代码让测试通过就行了,因此我们完全可以直接返回测试所期望的那个值,即一个用来表示10美元的结构体:

再度运行测试,我们会在终端中看到一条简短而令人开心的回应:

其中最关键的词就是PASS,这表示我们的测试通过了!

1.4.2 JavaScript

打开test_money.js文件,找到const assert=require('assert');这行代码,在它下面紧接着定义一个空白的Dollar类:

运行test_money.js时,我们会看到这样一条错误消息:

有进步!这条错误消息清楚地告诉我们,目前还没有给这个叫作fiver的对象定义名为times的方法。于是,我们现在就向Dollar类添加这样的方法:

这次运行测试,我们会看到一条新的错误消息:

❶这是Node.js v16所给出的错误消息,如果用的是v14,那么错误消息会稍有不同。

我们的测试希望times方法能够返回一个带有amount属性的对象。然而刚才写times方法时却没有让该方法返回任何值,因此JavaScript会把返回值判定为undefined,这样一个值当然没有名叫amount的属性(其实不单是这个属性,它同样没有其他名字的属性)。

JavaScript语言的函数与方法不会明确声明返回值的类型。如果某个函数根本就不返回任何东西,而我们又查看了该函数的返回值,那么这样查出来的返回值就是undefined。

怎样才能让测试变绿?要想做到这一点,最简单的方法是什么?是不是可以考虑让times函数总创建一个表示10美元的对象并返回该对象?

现在就试试看。我们添加一个constructor(构造器),用来将本对象的amount属性初始化成指定的值,然后让times方法总是通过调用这个构造器来创建一个表示10美元的Dollar对象:

❶每当创建Dollar对象的时候,constructor都会得到调用。

❷以调用方所给的参数来初始化this.amount变量。

❸times方法需要接受一个参数。

❹采用最简单的办法实现该方法,也就是让它总是返回一个表示10美元的对象。

现在运行这段代码,看不到任何错误。这说明我们的测试变绿了!

assert包中的strictEqual方法与其他方法都只在断言失败的情况下才给出错误消息,如果测试成功,那么这些方法不会有任何输出。我们将在第6章改进这一点。

1.4.3 Python

由于Dollar还没有得到定义,因此我们需要在test_money.py文件里面定义这样一个类。我们把这个类写在TestMoney类的前面:

运行代码,我们会看到这样一条错误消息:

有进展!这条错误消息清楚地告诉我们:目前还没有办法用参数来初始化Dollar对象(我们在代码里面需要用5或10这样的参数来初始化相应的Dollar对象)。现在,编写一种最简单的初始化器(initializer)来解决这个问题:

运行测试,我们发现它所产生的错误消息已经变了:

我们在这里发现一条规律:尽管测试依然失败,但每次失败的原因都略有不同。一开始是因为没有定义Dollar类,于是我们定义这样一个类,后来又变成没有能够接收参数的构造器,于是我们定义这样一个能够接收参数的构造器。现在,又变成了没有times方法,每次的错误消息都促使我们改进现有的代码,把它推进到一个更好的状态之中。这正是TDD的特征,也就是以我们自己所控制的节奏稳步向前推进。

现在我们稍微提升一下速度,把两件事放到一起做:一是定义名为times的函数,二是用最简单的方式实现该函数,以便让整个测试变绿。那么,什么是最简单的方式呢?当然是让这个函数总返回测试所要求的结果,也就是返回一个表示10美元的对象。

❶只要一创建Dollar对象,__init__函数就会得到调用。

❷以调用方给出的参数来初始化self.amount变量。

❸让times方法接受一个参数。

❹我们用最简单的方式实现times方法,也就是总让该方法返回一个表示10美元的Dollar对象。

运行测试,我们会看到一段简短而可喜的回应:

这里面写的时间可能稍微有点夸张,运行测试所需的时长应该要比0.000s多。但是别忘了,我们的重点是OK。这表示我们的第一个测试已经变绿了!