![测试驱动开发:入门、实战与进阶](https://wfqqreader-1252317822.image.myqcloud.com/cover/702/48593702/b_48593702.jpg)
1.4 让测试通过
我们刚才写测试的时候,暂时忽略了语法上有可能出现编译错误的所有地方,只想着把自己期望的结果表达出来。这样做合适吗?
在刚刚开始的时候,通过极少量的代码来摆正我们的前进方向确实是合适的,我们目前所处的正是这种刚刚开始的状态。当然,由于还没定义Dollar就开始使用它,因此测试会失败。这时有人可能会来一句:“那还用说?”然而大家目前还是得稍微有一点点耐心才行,因为我们至少做到了这样两点:
1.我们已经完成了第一步,也就是让第一个测试变红(或者说,写出了第一个失败的测试)。对于所要实现的每一个功能来说,编写失败的测试都是实现该功能时的第一步,而我们现在所要实现的是整个程序的第一个功能,因此我们不仅处在这个功能的起点,而且处在整个程序的起点。
2.我们可以(而且乐意)在开发后续功能的时候,逐渐提升实现每个功能的速度。然而我们同时也知道在需要放慢脚步的时候可以慢下来。
RGR环的第二个环节是让测试变绿(也就是令其通过)。
我们显然需要抽象出这个名为Dollar的概念。这一节就定义如何引入此抽象和其他一些必要的抽象,让我们的测试能够通过。
1.4.1 Go
在money_test.go末尾添加一个空白的Dollar结构体:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/48_03.jpg?sign=1738862952-2fi1ocwrrRvWZSFUVSZeRNRks94KYCQy-0-d8490511aefc70d962fa5b0e809418d5)
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/48_04.jpg?sign=1738862952-5Vyt2u41RFcwTDeFVaLfJQs5zNEmH4Oh-0-e4f81f3859bf2bd6804fdc62d76881b8)
这次运行测试,我们会看到一条新的错误消息:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/48_05.jpg?sign=1738862952-Gy4UT8DbHC8DFAdALaiJmh7GRmD0gkiK-0-060fecde358391ff8aa4e582d0fb8132)
不错,有进展了!
这条错误消息指引我们给Dollar结构体添加名为amount的字段。我们现在就做。对于当前的目标来说,只需要用int类型设计这个字段就足够了:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/49_01.jpg?sign=1738862952-omwAcLKF5U8CSyNBbEqLw9pma8P82KOq-0-dbe5ddfa609b2cccb77b5dc94287c385)
把这个字段添加到Dollar结构体之后,接下来运行测试的时候当然就会遇到这样一条错误消息:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/49_02.jpg?sign=1738862952-edOU7DVZ3jMMZZkNAPswTdw1z6nTOlug-0-77bf47c7a85dfd5e7a03352ff244e3fc)
大家可以看到一条规律:如果还没有定义某个东西(例如某个字段或方法)时就使用它,那么Go语言的运行时库会给出undefined错误。以后我们会利用这条规律来提升TDD的速度,从而更快地走完每一个RGR环。现在,我们先添加名叫Times的函数。根据我们所写的测试,这个函数必须接受一个(表示乘数的)数字,并返回另一个(表示相乘结果的)数字。
然而,这个结果应该怎样计算呢?我们当然知道基本的算术规则,也就是说,我们知道怎样用编程手段来计算两数相乘之积。但是,现在只需要用最简单的代码让测试通过就行了,因此我们完全可以直接返回测试所期望的那个值,即一个用来表示10美元的结构体:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/49_03.jpg?sign=1738862952-wh9U1ppcOoTWHO4FQL8lTSuhHmmFNwFW-0-fe4b529b9c046b4aee1a0363b0ab110a)
再度运行测试,我们会在终端中看到一条简短而令人开心的回应:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/49_04.jpg?sign=1738862952-bW53hMh9wY5QkQcdaL8uq1o2z4MKvgHj-0-5e2e8fa4375732e20b91ed764aba6e63)
其中最关键的词就是PASS,这表示我们的测试通过了!
1.4.2 JavaScript
打开test_money.js文件,找到const assert=require('assert');这行代码,在它下面紧接着定义一个空白的Dollar类:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/49_05.jpg?sign=1738862952-wPbGpQ5BTaouzK7VGMSHTkGrCg3a1rOd-0-5fdde3b7ba98260582d58c79db592820)
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/49_06.jpg?sign=1738862952-0GnzShv9aZVUbXwktFItEY8m3TCtrW0D-0-604dd0db77a106fe3cfca4da702d3978)
运行test_money.js时,我们会看到这样一条错误消息:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/50_01.jpg?sign=1738862952-FtovEhVRHia7Al045xnKyL8MDnUAM1Xm-0-3dc3e89646debdae37fb421fe83b43cb)
有进步!这条错误消息清楚地告诉我们,目前还没有给这个叫作fiver的对象定义名为times的方法。于是,我们现在就向Dollar类添加这样的方法:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/50_02.jpg?sign=1738862952-XYVa3X8gS0mh6r6AkO4DrddIRbssuTxy-0-6b873035cb7b64e01516b16f0d7f859a)
这次运行测试,我们会看到一条新的错误消息:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/50_03.jpg?sign=1738862952-KuTDzivMJyP139ZH58kBttVQo6L9RrTP-0-b3eb52b8c01d149e63c8d2d14cf62cf5)
❶这是Node.js v16所给出的错误消息,如果用的是v14,那么错误消息会稍有不同。
我们的测试希望times方法能够返回一个带有amount属性的对象。然而刚才写times方法时却没有让该方法返回任何值,因此JavaScript会把返回值判定为undefined,这样一个值当然没有名叫amount的属性(其实不单是这个属性,它同样没有其他名字的属性)。
JavaScript语言的函数与方法不会明确声明返回值的类型。如果某个函数根本就不返回任何东西,而我们又查看了该函数的返回值,那么这样查出来的返回值就是undefined。
怎样才能让测试变绿?要想做到这一点,最简单的方法是什么?是不是可以考虑让times函数总创建一个表示10美元的对象并返回该对象?
现在就试试看。我们添加一个constructor(构造器),用来将本对象的amount属性初始化成指定的值,然后让times方法总是通过调用这个构造器来创建一个表示10美元的Dollar对象:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/50_05.jpg?sign=1738862952-nHfvsyHS8DtOs5zr8Mj85zxI3Fuc7RXd-0-6b7029f9e48c8b77ef85d138ceda9b63)
❶每当创建Dollar对象的时候,constructor都会得到调用。
❷以调用方所给的参数来初始化this.amount变量。
❸times方法需要接受一个参数。
❹采用最简单的办法实现该方法,也就是让它总是返回一个表示10美元的对象。
现在运行这段代码,看不到任何错误。这说明我们的测试变绿了!
assert包中的strictEqual方法与其他方法都只在断言失败的情况下才给出错误消息,如果测试成功,那么这些方法不会有任何输出。我们将在第6章改进这一点。
1.4.3 Python
由于Dollar还没有得到定义,因此我们需要在test_money.py文件里面定义这样一个类。我们把这个类写在TestMoney类的前面:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/51_02.jpg?sign=1738862952-rs6gwrzyHMKay02t1nJ6SDv0J7zkrPXd-0-8763ca62e0400d17014b54ec0cac0556)
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/51_03.jpg?sign=1738862952-SvZiDGmsBpcfbsExRCggqmyKr2TGgoUT-0-d68ec298b5f326fdc000dc3d198318e6)
运行代码,我们会看到这样一条错误消息:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/51_04.jpg?sign=1738862952-O2bIXZSK1QbLMYpdyNtHKdETsIQ2WLSi-0-1b112078b87a60e04ff37a10408a0cd9)
有进展!这条错误消息清楚地告诉我们:目前还没有办法用参数来初始化Dollar对象(我们在代码里面需要用5或10这样的参数来初始化相应的Dollar对象)。现在,编写一种最简单的初始化器(initializer)来解决这个问题:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/51_05.jpg?sign=1738862952-u5QplUPPfOdVM9bxQX3MBI9zzKuMzJtn-0-a1151a65c2a35bbfcab736229eed7b83)
运行测试,我们发现它所产生的错误消息已经变了:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/51_06.jpg?sign=1738862952-u7EsqoO2ubqV4B1DqUR1sZ6cuzrUfN6R-0-0fbb7ac91a84a62353ef6aa3fb9d3663)
我们在这里发现一条规律:尽管测试依然失败,但每次失败的原因都略有不同。一开始是因为没有定义Dollar类,于是我们定义这样一个类,后来又变成没有能够接收参数的构造器,于是我们定义这样一个能够接收参数的构造器。现在,又变成了没有times方法,每次的错误消息都促使我们改进现有的代码,把它推进到一个更好的状态之中。这正是TDD的特征,也就是以我们自己所控制的节奏稳步向前推进。
现在我们稍微提升一下速度,把两件事放到一起做:一是定义名为times的函数,二是用最简单的方式实现该函数,以便让整个测试变绿。那么,什么是最简单的方式呢?当然是让这个函数总返回测试所要求的结果,也就是返回一个表示10美元的对象。
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/52_01.jpg?sign=1738862952-7oZJjQ5hVkYUe72narby40mTvar9xAJr-0-f21fd7d554d13dd16bc75f1d2c9d0fda)
❶只要一创建Dollar对象,__init__函数就会得到调用。
❷以调用方给出的参数来初始化self.amount变量。
❸让times方法接受一个参数。
❹我们用最简单的方式实现times方法,也就是总让该方法返回一个表示10美元的Dollar对象。
运行测试,我们会看到一段简短而可喜的回应:
![](https://epubservercos.yuewen.com/2F0B0F/28235549002740206/epubprivate/OEBPS/Images/52_02.jpg?sign=1738862952-A9gShLUi6iaYqsXpxN9mvXsYwl3L9of4-0-c6ac1b0920c8134c248d5aac3637622a)
这里面写的时间可能稍微有点夸张,运行测试所需的时长应该要比0.000s多。但是别忘了,我们的重点是OK。这表示我们的第一个测试已经变绿了!