通常来说项目内使用单元测试或者TDD的最主要难点是什么?如何写测试,语法不是问题,我们可以通过几个例子十几分钟就可以学会,但是如何把脑子里自己对要做的事情的一些模糊的想法转化成可用的测试,甚至是需要在功能实现之前就考虑清楚怎么去验证他们,多数人这时候是不知道如何下手的。
有人告诉你应该使用TDD,但是该如何给一个还没有实现的东西写测试呢?在甚至都还没想明白这个函数要实现什么功能,或者到底是要用一个函数还是两个函数呢,所有我们听说的只是TDD需要换一种软件开发的思维方式,但是应该怎么做呢?
现在我来告诉你!
如何将脑中的模糊想法转化为测试
让我引导您了解一个可以用来完成此任务的方法,一种将任何你想实现的想法转化成有形的、可测试的用例的方法。我已经使用了很久的测试驱动开发方法,这个方法就是基于我在做TDD的过程中所经历的那种思考的过程。
这个方法与TDD采用的技术细节无关,它更关注经验丰富的TDD开发人员在编码过程中所经历的思考过程,了解这个思考过程可以让TDD的实践变得更容易。
切记:最主要的是要调整你的思维方式,这个过程开始会需要你付出一些有意识的努力,当你付出足够努力的时候,它就会变成你的习惯,编写测试代码将和你写If-else一样平常而必须,你再也不需要为此做额外的努力。
接下来我们以一个例子开始:计算一个密码的强度
这个时候我和你一样,只是想到了这个需求,完全还没有头绪,之前也没有写过任何关于这个功能的代码。我现在也处于完全茫然的状态
在正式开始之前,我想再强调一个重要是事儿:我们的目的不是追求完美,测试驱动的开发是一个反复的过程,这意味着您只需执行少量重复步骤(红-绿-重构),这个过程我们不用过多在意细节,因为本来软件开发就是一个经常变化的过程,TDD让我们做任何改动都更容易,所以我们不用一次追求100%准确,只需要不断重复TDD的过程,我们的软件会自然而然变成它该有的样子。
下面让我们一起开始吧
第一步:确定输入输出
我们从高层次开始这个过程。我们现在还不在乎如何实现。我们只需要关系我们的原始需求:计算密码强度,为了实现这个需求,我们需要输入一些东西给程序,然后根据输入我会得到一些输出。通常为了实现这个过程我们会思考着应该定义一个函数,并且会思考一下该函数需要输入哪些数据以及返回什么样的结果。请注意,以上只是我们的思考过程,到目前为止我们还没有写任何代码。
针对这个需求我们需要如何定义呢?
首先我们需要一个输入:输入必须是一个密码
输出也很简单:标识密码强度的一个值,为了简单起见,假设我们的密码强度只是一个标识密码是否是高强度密码的布尔类型。
第二步:确定函数签名
针对我们这个例子,很简单-只需输入密码即可。我们可以完全基于该值完成整个计算。那返回值呢?很简单,因为这是一个计算,所以我们可以直接返回结果。现在我们可以决定在代码中调用该函数的方式:
var strong = isStrongPassword('password string goes here');
第三步:选择要实现功能的一个微小的方面
现在,我们知道了目标,涉及的数据和功能签名。在非TDD工作流程中,可能现在就开始编写该功能的代码了。您可能已经对它的工作方式有了一些想法并且有极大的冲动去编码实现它。这是大多数人在使用TDD时遇到麻烦的地方。关于如何编写函数的所有这些想法会过早引起了您的注意,但是直到开始编写代码之前,我们都不确定如何布置代码。
这时候我们需要考虑的是:我们可以接近目标的最简单的行为是什么?
实际工作中,我们经常会遇到我们尝试解决非常大,例如本例中如果我们考虑密码强度,就会有不同规则的想法,例如特殊字符,数字,密码长度等。
那么,使该功能更接近于验证密码的最终目标,我们能采取的最简单的步骤是什么?
如果在没有TDD的情况下构建此函数,那么第一行(或两行)代码将是什么?
我们可以添加使功能更接近工作的最少代码量是多少?
在这个例子中密码强度最简单的规则可能是空密码。这真的很容易-密码为空时输出应始终为false。所以我们从这里开始。
第四步 写一个测试
注意前面的所有步骤实际上与没有TDD的情况下的区别,主要区别在于,我们将重点放在函数的调用方式以及结果上,而不是着眼于实现功能。也就是说–我们正在考虑该功能在某些情况下的行为。就像这样,我们已经可用开始编写测试了:
我们决定该功能将密码作为其唯一参数。我们还决定返回一个布尔值以指示密码是否强壮。
我们还选择了一个空密码,结果应该始终为false –表示一个空密码很弱。
现在我们创建第一个测试:
describe('isPasswordStrong', function() {
it('should give negative result for empty string', function() {
var password = '';
var result = isPasswordStrong(password);
expect(result).to.be.false;
});
});
注意,我们很容易地编写了此代码,而无需知道函数中确切的代码行是什么。给定一个空字符串作为参数,结果应为false。一个简单的行为,很容易转化为测试。
第五步 实现代码
我们只会添加使测试通过的最少量的代码。
function isPasswordStrong(password) {
if(!password) {
return false;
}
}
如果我们要继续开发密码强度功能,我们要做的就是重复一次。我们将回到第3步,然后选择下一步。步骤4,添加测试。步骤5,实施。重复。
如果您坚持继续像这样的小步前进,TDD过程就会变得容易得多。可能需要进行针对少量代码的多几次测试,但这并不是一件坏事。TDD通过这种方式帮助您减少您可能编写的无用代码量,因为您添加的每一行代码均已通过测试验证。
总结
从示例中可以看到,我们可以将相同的五个步骤应用于各种功能。如果您需要练习,可以从我们在此处采用的例子开始,然后查看是否可以采用5个步骤使这些功能完全发挥作用。
一旦掌握了基本知识,测试驱动开发就不会困难。挑战在于,这需要您转变思路:如果没有TDD,您将直接考虑如何实施某些事情。但是在TDD中您会考虑系统的行为是如何表现的 ,应该如何去验证。
函数的输入是什么,调用函数想要的输出(行为)是什么?
确定如何从代码中调用函数
为您可以想到的某些输入选择最小的行为
编写测试,使用这些输入来调用函数并验证行为
实施足够的代码以使测试通过
如果我们遵循这些简单的步骤,则提前编写测试变得容易得多。在继续处理代码时,您只需在步骤3至5之间重复即可。
请记住–第一次尝试时就不需要完美,开始就追求完美只会让你过早陷于困境。这也不只是TDD的目的:无论开始做的如何,您可能都需要重做和重构代码的一部分,TDD只是使其更安全,因为您已经进行了一些测试,以验证代码的更改不会导致代码损坏。
更多文章和工具请描我的微信二维码添加关注@敏捷新视界