一旦了解了如何使用一个单元测试框架编写结构化、可维护和可靠的测试,下一个问题就是什么时候编写测试。很多人觉得为软件编写单元测试的最佳时机是软件编码完成以后,但是越来越多的人选择在产品代码编写之前写单元测试。这种方法称为测试优先或是测试驱动开发(Test-Driven Development,TDD)。 注意 对于测试驱动开发的具体含义,人们有很多不同的观点。有人说测试驱动开发是测试优先开发,有人说测试驱动就是进行大量的测试。有人说测试驱动开发是一种代码设计方法,其他人则觉得只有使用某些设计时,测试驱动开发才成为一种驱动代码行为的方法。要更全面地了解人们对TDD的观点,请参考我的博客文章“TDD的各种含义”(The various meanings of TDD,参见http://osherove.com/blog/2007/10/8/the-various-meanings-of-tdd.html)。在本书中,TDD意指测试优先开发,设计仅作为这项技术中的次要方面(本书中不讨论设计)。 图1-3和图1-4展示了传统编码方式和TDD之间的区别。 图1-3 编写单元测试的传统方法。虚线表示人们认为是可选的行为 图1-4 测试驱动开发概要图。注意,这是一个螺旋式的过程:编写测试,编码,重构,编写下一个测试。这张图展示了TDD的增量特性:小步骤的积累得 到高质量的最终结果 如图1-4所示,TDD和传统开发方式不同。你首先编写一个会失败的测试,然后创建产品代码,并确保这个测试通过,接下来是重构代码或者创建另一个会失败的测试。 本书关注的是编写优秀单元测试的技术,而非测试驱动开发方法,但是我非常推崇TDD。我曾经用TDD编写过几个重要的应用程序和框架,也管理过使用TDD的团队,还讲授过100多堂关于TDD和单元测试技术的课程和讲习班。在我的整个职业生涯中,我发现TDD能帮助创建高质量代码和高质量的测试,还能更好地设计代码。我相信TDD对你也会有帮助,但是你需要付出很多(学习的时间、实施的时间,以及更多),当然这些付出绝对是值得的。 我们必须认识到,TDD并不能保证项目一定成功,或者测试都健壮、可维护。人们很容易陷入TDD技术的细节,而忘记关注编写单元测试的方式:测试的命名,测试的可维护性和可读性,以及是否测试了正确的内容,或者测试本身是否有缺陷。这也是我要写这本书的原因。 TDD的技术相当简单。 (1) 编写一个会失败的测试,以证明产品中代码或者功能的缺失。编写测试的时候,要假设产品代码已经能工作 了,这样测试的失败就说明产品代码中有缺陷。如果要在一个计算器类中添加一个新功能,记住LastSum的值,我就会写一个测试来验证LastSum确实是正确的值。这个测试最初会编译失败,只有在添加了需要的代码后,编译才能通过(但是还没有实现记住数值的真正功能),然后测试可以运行,但是会失败,因为我还没有实现所需的功能。 (2) 编写符合测试预期的产品代码,使测试通过。产品代码应该尽量简单。 (3) 重构代码。如果测试通过了,你就可以编写下一个单元测试,或者进行重构,使代码可读性更强,或者去除重复代码等。 重构可以在编写多个测试之后进行,也可以在每个测试后都进行。重构是一项重要的实践,它确保代码更易读,更好维护,同时还依然能通过之前编写的所有测试。 定义 重构意味着在不改变一段代码功能的前提下修改代码。如果重命名了一个方法,你就进行了重构。如果把一个较大的方法分成了多个较小的方法调用,你也对代码进行了重构。代码还是具有同样的功能,但重构后变得更容易维护、理解、调试和修改。 这些步骤听起来很技术性,但是其间蕴藏着很多的智慧。如果实施得当,TDD可以极大地提高代码质量,减少缺陷数量,提升你对代码的信心,缩短发现缺陷的时间,优化代码设计,让你的经理更满意。但如果实施不当,TDD可能导致项目延期,浪费时间,打击士气,降低代码质量。TDD是一把双刃剑,很多人在失败之后才体会到这一点。 确切地说,没有人会告诉你TDD有一个最大的优点:如果一个测试失败了,没有经过修改再次运行又成功了,你其实是在测试这个测试本身。如果你预期一个测试会失败,但它却成功了,那测试本身可能有缺陷,或者测试的对象不对。如果一个测试之前失败了,你现在预期它成功,它却依然失败,那测试可能有缺陷,或者测试预期的结果不正确。 本书讨论的是可读、可维护并且可靠的测试,但是只有当你看到测试该成功的时候成功,该失败的时候失败,才能获得对它们最大的确定。在这方面,TDD对你会有很大帮助,这也是为什么和编码之后进行单元测试相比,开发人员在使用TDD开发时进行代码调试要少得多。如果开发者信任测试,他们不会为了只是以防万一而调试代码。这种信任只能通过看到测试的两方面获得:该失败的时候失败,该成功的时候成功。