At the Forge - 控制器的 RSpec
RSpec 是 Ruby 程序员常用的测试框架,它基于行为驱动开发 (BDD) 原则。BDD 与测试驱动开发 (TDD) 的区别在于,它从外部而非内部看待程序,将代码视为用户或观察者,而不是实现者。在 BDD 世界中,您不实现测试,而是实现规范;如果规范通过,则代码正在执行其应执行的操作。
与 Ruby 领域的许多事物一样,RSpec 在 Web 开发的 Rails 框架用户中变得特别流行。上个月,我讨论了 RSpec 在测试 Rails 模型(即连接到关系数据库的类)方面的应用。本月,我将研究稍微复杂一些的控制器测试案例。控制器测试更加复杂,因为它需要您考虑更多的情况,或者至少是不同的情况。现在您必须考虑来自外部世界的输入,以 HTTP 请求的形式。它还引入了对模拟 (mocks) 和桩 (stubs) 的需求,您可以使用这些对象来测试您的控制器,而无需创建真实的对象(以及其背后的数据库)。
本月,我将研究 RSpec 测试框架允许您在 Ruby on Rails 应用程序中测试控制器的一些方法。在此过程中,我将考虑测试控制器的意义以及您可能想要测试它们的程度。最后,我将快速浏览一下模拟和桩的世界,并展示它们如何帮助改进您的测试。
上个月,我开始构建一个简单的预约日历作为示例。碰巧的是,我只实现了该预约日历的一小部分,创建了一个单独的人员模型,您可以使用它来表示您将要会见的人员。现在,让我们也创建预约
./script/generate rspec_model appointment starting_at:timestamp \ ending_at:timestamp person_id:integer location:text notes:text
正如您可能预期的那样,您将通过将模型文件链接在一起,指示每个人都有多个预约,但每个预约都属于一个人,从而增强您的模型文件。这将允许您使用 Ruby 的面向对象语法来检索 person.appointments 或 appointment.person。
现在您已经有了两个模型,您应该对它们做一些事情。一个显而易见的事情是列出今天的预约。以 BDD 方式,让我们编写一个规范来描述系统应该做什么;您实际上将在之后实现代码。
该规范将描述您希望如何能够查看预约列表。假设模型(人员和预约)的规范已到位,并且您现在可以专注于您的控制器。基本上,您需要一个预约控制器,其索引操作显示所有当前预约。您可以通过生成这样的控制器来完成此操作
./script/generate rspec_controller appointment index new create show
创建一个名为 appointment 的控制器,以及一些与纯 RESTful 控制器类似的操作(但这不是)。现在,打开 spec/controllers/appointment_controller_spec.rb,这是此控制器的规范文件位置,您将看到许多简单的规范,每个规范对应于您定义的每个方法。正如我上个月解释的那样,RSpec 的强大之处在于其可读性,其中“describe”块指示整体上下文,“it”块描述规范,然后是单独的断言,这些断言被写成“something SHOULD be-something”。因此,索引操作的初始自动生成规范如下所示
describe "GET 'index'" do it "should be successful" do get 'index' response.should be_success end end
响应对象在控制器规范中自动给出,它允许您执行诸如检查成功之类的操作。问题是,您还希望此索引操作检索(并显示)数据库中的所有当前预约。您如何测试这一点?
一种方法是用一堆虚假数据或“fixtures”加载您的数据库,并实际从数据库中检索数据。但是,嘿,您正在尝试在此处测试控制器,而不是数据库——因此访问数据库将是过度的。
您可以做的另一件事是告诉 Ruby 您期望控制器请求一堆预约对象。实际上,它应该按照您的规范请求数据库中的所有预约。只要它这样做,您就可以确信该操作的规范已得到满足。
您可以通过将您的正常 Appointment 对象切换为模拟 (mock) 对象(有时称为测试替身对象)来做到这一点。此模拟对象允许您检查是否发生了正确的事情,同时保持在您的程序范围内。例如,如果您想确保调用了 Appointment.find(:all),请修改您的规范以如下方式读取
describe "GET 'index'" do it "should be successful" do appointments = [mock(:appointment), mock(:appointment)] Appointment.should_receive(:find).and_return(appointments) get 'index' response.should be_success end end
在这里,在调用“get 'index'”之前添加了两行。在第一行中,您创建了一个包含两个模拟对象的数组,每个对象都将声明(如果被询问)它是 Appointment 的实例。它当然不是真正的预约对象,而是一个仅用于测试的薄层。您将创建两个这样的对象,以便您可以假装数据库中有多个预约。
下一行更有趣。它表示 Appointment(类)应该期望在某个时候接收到 find 方法。请注意,此处的放置位置很重要;如果您在调用 GET 之后放置此模拟行,则为时已晚。相反,设置模拟,以便 GET 方法可以适当地执行操作。如果模拟未收到“index”的调用,则 RSpec 以致命错误退出。实际上,使用 BDD 方法,这正是我在运行 RSpec 后可以期望看到的
Spec::Mocks::MockExpectationError in 'AppointmentController ↪GET 'index' should be successful' <Appointment(id: integer, starting_at: datetime, ending_at: ↪datetime, person_id: integer, location: text, notes: ↪text, created_at: datetime, updated_at: datetime) ↪(class)> expected :find with (any args) once, ↪but received it 0 times
换句话说,上面的示例表示您希望调用 Appointment 的“find”方法,但这从未发生过。因此,将 find 的调用添加到 index 操作中
def index Appointment.find(:all) end
现在规范通过了(感谢模拟对象),并且您拥有了功能。有什么比这更好的吗?好吧,也许您想测试在显示该对象的视图中看到的输出。我不会在此处深入探讨,但 RSpec 也允许您测试视图,使用类似的机制来查看生成的 HTML 输出。
实际上,我才刚刚开始触及 RSpec 模拟机制可能实现的功能的表面。您可以桩 (stub) 出特定的对象方法,从而允许您在不使用其开销或依赖项的情况下使用模型。例如,您可以用您返回的模拟对象替换对“find”的调用,并忽略对“save”的任何调用——从而允许您使用真实模型,但更快更可靠。
您还可以想象如何使用模拟来测试您检索彼此关联的模型的能力。例如,“index”方法如果仅显示预约,则可能毫无用处。您可能希望显示与预约安排的人员。这需要遍历外键关联,您可以使用桩对象轻松处理,然后从您的模拟中引用这些对象。
现在,您可能想知道使用 fixtures 或 factories 是否可以实现所有这些。答案是肯定的,多年来不同的开发人员都成功地使用了 fixtures 和 factories。我通常发现 fixtures 是最自然、最容易理解和使用的,但是随着项目变得越来越大,它们通过数据库并要求我设置和协调每个单独的对象的事实开始付出代价。我也喜欢使用 factories,并且一直在试验(正如我几个月前提到的那样)不同的 factory 类。
但是,我接触模拟越多,我就越想知道整个 factory 类是否是必要的,或者我是否可以简单地使用模拟和桩来精确定位和使用我感兴趣的功能。我确信其他开发人员也在考虑这些因素,我希望 Ruby 开发人员可用的众多选项将改进和鼓励在 Ruby 社区中已经非常强大的测试文化。
RSpec 的“由外而内”的测试方法需要一些时间来适应,但我越来越发现它是一种迫使我更深入地思考我的代码以及我的测试策略的方法。也就是说,我不确定我是否真的对 RSpec 优于类似的 BDD 风格工具(例如 Shoulda,它与 Ruby 的传统 Test::Unit 系统一起工作)有强烈的偏好。最重要的是,您应该尝试在您设计的任何软件中尽可能多地包含自动化测试——这不仅是因为它将使您的用户受益,而且还将使您作为开发人员受益。
资源
RSpec 的主页是 rspec.info,它包含安装和配置文档,以及指向其他文档的指针。
Pragmatic Programmers 最近发布了一本书,名为 The RSpec Book,由 RSpec 维护者 David Chelimsky 和许多其他积极参与 RSpec 社区的人员编写。如果您有兴趣使用 RSpec(或其近亲 BDD 工具 Cucumber),本书是一个极好的起点。
RSpec 邮件列表(很有帮助和友好,但数量相当大)位于 groups.google.com/group/rspec。
最后,The Rails Way(我最喜欢的关于 Rails 的书籍之一,由 Obie Fernandez 撰写)中对 RSpec 和模拟进行了很好的介绍。本书描述了在 RSpec 上下文以及作为开发 Rails 应用程序时的一般测试工具的模拟。
Reuven M. Lerner,一位资深的 Web/数据库开发人员和顾问,是西北大学学习科学博士候选人,研究在线学习社区。在芝加哥地区生活四年后,他最近(与妻子和三个孩子)返回了他们在以色列莫迪因的家。