6. },

  7.

  8. "test 8 day difference should result in '1 week ago'": function () {

  9. assertEquals("1 week ago", dateUtil.differenceInWords(this.date8DaysAgo));

  10. },

  11.

  12. "test should display difference with days, hours, minutes and seconds": function () {

  13. assertEquals("3 days, 2 hours, 16 minutes and 10 seconds ago",

  14. dateUtil.differenceInWords(this.date3DaysAgo));

  15. }

  16. });

  现在,在我们的测试中没有了DOM元素,而我们能更有效的测试生成正确字符串的逻辑。同样的,测试这个jQuery插件的问题是确信文本内容被替换。

  为什么为测试而修改代码?

  每次我向别人介绍测试和解释可测试性的概念,总是听到关于“难道你不仅让我用更多的时间写这些测试,而且我还得为了测试改变我的代码吗?”的说词。

  来看我们刚才为人性化时间差而做的改变。改变是为了方便测试的目的,但你能说只有测试受益吗?恰恰相反,改变使代码更易于分离无关行为。现在,如果我们晚点决定执行如Twitter反馈到我们的页面,我们能直接使用时间戳调用differenceInWords 函数,而不是通过DOM元素和jQuery插件的笨拙的路线(Now, if we later decide to implement e.g. aTwitter feed on our pages, we can use the differenceInWords functiondirectly with the timestamp rather than going the clumsy route via a DOMelement and the jQuery plugin.)。可测试性是良好设计的固有特性。当然,你可以有可测试性和不好的设计,但你不能有一个良好的设计而不具有可测试性。考虑作为一个小例子的情况的测试—你的代码的例子—如果测试很困难,也意味着使用代码很困难。

  先写测试:测试驱动开发

  当你在现有的代码中使用单元测试时,大的挑战是可测试性问题。为了持续提高我们的工作流程,我们能做什么?这引出了一个让可测试性直接进入产品代码灵魂的万无一失的方法是先写测试。

  测试驱动开发(TDD)是一个开发过程,它由一些小迭代组成,并且每个迭代通常由测试开始。直到有一个失败的单元测试需要,否则不写产品代码。TDD使你关注行为,而不是你下一步需要什么代码。

  比方说,我们被告知那个计算时间差的jQuery插件需要计算任意两个时间的差,而不只是和当前时间的差值。你如何使用TDD解决这个问题?好了,第一个扩展是提供用于比较的第二个日期参数:

  [javascript] view plaincopy1. "test should accept date to compare to": function () {

  2. var compareTo = new Date(2010, 1, 3);

  3. var date = new Date(compareTo.getTime() - 24 * 60 * 60 * 1000);

  4.

  5. assertEquals("24 hours ago", dateUtil.differenceInWords(date, compareTo));

  6. }

  这个测试假想该方法已经接受两个参数,并预期当比较两个传过去日期恰好有24小时的差别时,结果字符串为"24 hours ago"。运行该测试不出所料的提示它不能工作。为让测试通过,我们不得不为该函数添加第二个可选参数,同时确保没有改变函数使现有的测试失败。下面是一个实现的方法:

  [javascript] view plaincopy1. dateUtil.differenceInWords = function (date, compareTo) {

  2. compareTo = compareTo || new Date();

  3. var diff = compareTo - date;

  4.

  5. // ...

  6. };

  所有的测试都通过了,说明新的和原来的需求都得到满足了。

  现在我们接受两个日期,我们可能希望方法能描述的时间差是过去或将来。我们先用另一个测试来描述这个行为:

  [javascript] view plaincopy1. "test should humanize differences into the future": function () {

  2. var compareTo = new Date();

  3. var date = new Date(compareTo.getTime() + 24 * 60 * 60 * 1000);

  4.

  5. assertEquals("in 24 hours", dateUtil.differenceInWords(date, compareTo));

  6. }

  让这个测试通过需要一些工作量。幸运的是,我们的测试已经覆盖(部分)我们之前的要求。(两个单元测试很难构成良好的覆盖,但假想我们已经有针对该方法的完整的测试套件)。一个强大的测试套件让我们不害怕改变代码,如果我们打破它了,我们知道会得到告警。我的终实现是这样的:

  [javascript] view plaincopy1. dateUtil.differenceInWords = function (date, compareTo) {

  2. compareTo = compareTo || new Date();

  3. var diff = compareTo - date;

  4. var future = diff < 0;

  5. diff = Math.abs(diff);

  6. var humanized;

  7.

  8. if (diff > units.month) {

  9. humanized = "more than a month";

  10. } else if (diff > units.week) {

  11. humanized = format(Math.floor(diff / units.week), "week");

  12. } else {

  13. var pieces = [], num, consider = ["day", "hour", "minute", "second"], measure;

  14.

  15. for (var i = 0, l = consider.length; i < l; ++i) {

  16. measure = units[consider[i]];

  17.

  18. if (diff > measure) {

  19. num = Math.floor(diff / measure);

  20. diff = diff - (num * measure);