65.9K
CodeProject 正在变化。 阅读更多。
Home

Rails 3实战 - 测试驱动开发

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (2投票s)

2011年9月26日

CPOL

8分钟阅读

viewsIcon

18751

来自Rails 3实战的一章摘录

image002.jpg Rails 3 in Action
作者:Yehuda Katz 和 Ryan A. Bigg

一个含糊但真实的答案是:“为什么我要写测试?”“因为你是人。”因为人会犯错,所以拥有一个工具来告知他们何时犯错会很有帮助,不是吗?在这篇基于《Rails 3 in Action》第二章的文章中,作者们将向你展示如何通过测试驱动开发来挽救你的项目。

您可能还对以下内容感兴趣…

一个含糊但真实的答案是:“为什么我要写测试?”“因为你是人。”人类——本书大部分读者——都会犯错。这是我们学习的一种方式。因为人会犯错,所以拥有一个工具来告知他们何时犯错会很有帮助,不是吗?自动化测试提供了一个快速的安全网,可以在开发人员犯错时告知他们。这里的“他们”当然指的是你。

我们希望你犯的错误越少越好。我们希望你能挽救你的项目!TDD 和 BDD 也能让你在编写任何代码之前有时间思考你的决定。通过先为实现编写测试,你正在(或者至少应该)思考实现:你将在测试之后编写的代码以及你将如何让测试通过。如果你发现测试难以编写,那么实现可能可以改进。不幸的是,除了咨询熟悉该过程的其他人之外,没有明确的方法可以量化编写测试和解决该过程的难度。

一旦测试实现,你就应该开始编写一些能让你的测试通过的代码。如果你发现自己倒着工作——重写你的测试以适应有 bug 的实现——通常最好重新思考测试并放弃实现。先测试,后编写代码。

为什么测试?

自动化测试比手动测试容易得多。你是否曾经浏览过一个网站并手动填写一个带有特定值的表单,以确保它符合你的预期?让计算机来完成这项工作不是更快、更容易吗?是的,正是自动化测试的魅力所在:你不会花费时间手动测试你的代码,因为你已经编写了测试代码来为你完成这项工作。

万一你弄坏了什么东西,测试就在那里告诉你哪里、何时、如何以及为什么坏了。虽然测试永远无法 100% 保证,但你事先没有编写测试来获得这些信息的几率为 0%。没有什么比通过一个愤怒的客户在清晨打来的电话才发现东西坏了更糟糕的了。测试通过提供你和你的客户安心来防止这种情况的发生。如果测试没有坏,那么实现很可能(虽然不保证)也没有坏。

你很可能在某个时候会遇到这种情况:当用户尝试执行一个你未在测试中考虑到的操作时,你的应用程序中的某些东西会坏掉。有了基础测试,你就可以轻松地复制用户遇到的故障场景,生成自己的失败测试,并利用这些信息来修复 bug。这种常用做法称为回归测试。

在应用程序中拥有扎实的测试基础非常重要,这样你就可以花时间正确地开发新功能,而不是修复那些你做得不太好的旧功能。一个没有测试的应用程序很可能在某个方面存在问题。

编写你的第一个测试

Ruby 的第一个测试库是 Test::Unit,由 Nathaniel Talbott 在 2000 年编写,现在是 Ruby 核心库的一部分。该库的文档对它的目的进行了精彩的概述,正如本人所总结的:

单元测试的总体思想是,你编写一个测试方法,该方法对你的代码做出某些断言,并作用于一个测试夹具。一堆这样的测试方法被打包成一个测试套件,并可以在开发人员希望的任何时候运行。运行的结果被收集在测试结果中,并通过某个 UI 显示给用户。

—Nathaniel Talbott

Talbott 引用的 UI 可以是终端、网页,甚至是灯。[1]

在 Ruby 世界中,你希望至少现在已经遇到过的一种常见做法是让库为你做很多繁重的工作。当然,你可以自己编写一个文件,该文件加载你的另一个文件并运行一个方法,然后确保它工作正常,但既然 Test::Unit 以如此低的成本提供了这项功能,为什么还要这样做呢?当有人为你做好时,永远不要重复造轮子。

现在你要编写一个测试,稍后你将为它编写代码。欢迎来到 TDD。

要尝试 Test::Unit,首先创建一个名为 example 的新目录,并在该目录中创建一个名为 example_test.rb 的文件。以 _test 作为文件名后缀是一个好习惯,这样从文件名就可以清楚地知道它是一个测试文件。在此文件中,你将定义最基本的测试,如下面的列表所示。

列表 1 example/example_test.rb

require 'test/unit'

class ExampleTest < Test::Unit::TestCase
  def test_truth
    assert true
  end
end

要使其成为 Test::Unit 测试,你首先需要 require test/unit,它是 Ruby 标准库的一部分。这提供了下一个类所继承的 Test::Unit::TestCase 类。继承此类提供了运行此类中任何以 test 开头的方法的功能。此外,你还可以使用 test 方法定义测试。

test "truth" do
  assert true
end

要运行此文件,请在终端中运行 ruby example_test.rb。当此命令完成后,你会看到一些输出,其中最相关的是中间的两行。

.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

第一行是一个单独的点。这是 Test::Unit 表示它运行了一个测试并且测试通过的方式。如果测试失败,它将显示为 F;如果出错,则显示为 E。第二行提供了关于发生了什么的统计信息,具体来说,有一个测试和一个断言,没有失败,没有错误,也没有跳过任何内容。太成功了!

测试中的 assert 方法进行一个断言,即传递给它的参数求值为 true。此测试在参数不是 nil 或 false 时通过。当此方法失败时,它会使测试失败并引发异常。来吧,试试将 1 放在那里而不是 true。它仍然有效。

.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

在下面的列表中,你将删除方法开头的 test_,并将其定义为简单的 truth 方法。

列表 2 example/example_test.rb,备用的 truth 测试

def truth
  assert true
end

Test::Unit 告诉你没有指定测试,因为它运行了 Test::Unit 内部的 default_test 方法。

No tests were specified.
1 tests, 1 assertions, 1 failures, 0 errors

请记住,始终在 Test::Unit 方法前加上 test!

挽救培根

让我们通过创建一个 bacon_test.rb 文件并编写如下列表所示的测试来使其更复杂一些。

列表 3 example/bacon_test.rb

require 'test/unit'
class BaconTest < Test::Unit::TestCase
  def test_saved
    assert Bacon.saved?
  end
end

当然,你想确保你的培根[2] 总是被保存,这就是你做到这一点的方式。如果你现在运行命令来运行此文件,ruby bacon_test.rb,你将收到一个错误。

NameError: uninitialized constant BaconTest::Bacon

你的测试正在寻找一个名为 Bacon 的常量,但找不到它,因为你还没有定义这个常量。对于这个测试,你想要定义的常量是一个 Bacon 类。

你可以在测试之前或之后定义这个新类。请注意,在 Ruby 中,你通常必须在常量和变量之前定义它们。在 Test::Unit 测试中,代码只有在完成评估后才会运行,这意味着你可以在测试之后定义 Bacon 类。在下一个列表中,你遵循更传统的在测试上方定义类的这种方法。

列表 4 example/bacon_test.rb

require 'test/unit'
class Bacon

end
class BaconTest < Test::Unit::TestCase
  def test_saved
    assert Bacon.saved?
  end
end

重新运行测试后,你会得到一个不同的错误。

NoMethodError: undefined method `saved?' for Bacon:Class

有进展!它识别出现在有一个 Bacon 类,但该类没有 saved? 方法,所以你必须定义一个,如以下列表所示。

列表 5 example/bacon_test.rb

class Bacon
  def self.saved?
    true
  end
end

再次运行 ruby bacon_test.rb,你就可以看到测试现在通过了。

.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

你的培根确实被保存了!现在,每当你想要检查它是否已保存时,都可以运行此文件。如果有人进来并将 true 值更改为 false,那么测试就会失败。

F
  1) Failure:
test_saved(BaconTest) [bacon_test.rb:11]:
Failed assertion, no message given.

当断言失败时,Test::Unit 会报告“断言失败,未给出消息”。你可能应该让错误消息更清晰!要做到这一点,你可以在测试中的 assert 方法中指定一个额外的参数,如下所示。

assert Bacon.saved?, "Our bacon was not saved :("

现在当你运行测试时,你会得到一个更清晰的错误消息。

  1) Failure:
test_saved(BaconTest) [bacon_test.rb:11]:
Our bacon was not saved :(

摘要

你刚刚了解了使用 Test::Unit 的 TDD 的基础知识。了解它很有用,因为它为 Ruby 中的 TDD 奠定了基础。Test::Unit 也是 Rails 的默认测试框架,所以你可能会在你的工作中遇到它。

这里有一些你可能感兴趣的 Manning 的其他书籍。

image003.jpg

Grails in Action
Glen Smith 和 Peter Ledbrook

image004.jpg

Griffon in Action
Andres Almiray 和 Danno Ferrin

image005.jpg

Spring in Action, Third Edition
Craig Walls

[1] 例如 GitHub 提供的:http://github.com/blog/653-our-new-build-status-indicator

[2] 比喻意义上的和酥脆的培根都算。

© . All rights reserved.