Ruby on Rails 2.1 - 单元测试

简介

在继续之前,让我们快速了解几个定义 −

  • 测试 − 它们是产生一致结果的测试应用程序,并证明 Rails 应用程序执行了预期的操作。测试与实际应用程序同时开发。

  • 断言 − 这是一行代码,用于评估对象(或表达式)以获得预期结果。例如 - 这个值 = 那个值吗?这个对象是否为 nil?

  • 测试用例 −这是一个从 Test::Unit::TestCase 继承的类,包含由上下文相关测试组成的测试策略

  • 测试套件 − 这是测试用例的集合。当您运行测试套件时,它将依次执行属于它的每个测试。

Rails 测试

当您运行辅助脚本 script/generate 来创建控制器模型时,Rails 会生成一个用于单元和功能测试的框架。通过在框架中填充您编写的功能的测试,您可以获得相当不错的测试覆盖率。在 Rails 应用程序中,有两个重要的测试点 −

  • 测试模型

  • 测试控制器

本教程将简要介绍这两种测试。因此,让我们创建一个 testapp 来理解这个概念。

C:
uby> rails -d mysql testapp

数据库设置

到目前为止,我们只使用了 Rails 应用程序的开发数据库,​​但现在您需要确保测试数据库也已创建,并且 config/database.yml 文件中的相应部分已正确设置。

让我们按如下方式创建开发和测试数据库 −

mysql> create database testapp_test;
Query OK, 1 row affected (0.01 sec)

mysql> create database testapp_development;
Query OK, 1 row affected (0.01 sec)

mysql> use testapp_test;
Database changed

mysql> grant all privileges on testapp_test.* 
   to 'root'@'localhost' identified by 'password';
Query OK, 0 rows affected (0.00 sec)

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)

配置 database.yml

按如下方式配置您的 config/database.yml −

development:
   adapter: mysql
   encoding: utf8
   database: testapp_development
   username: root
   password: password
   host: localhost
test:
   adapter: mysql
   encoding: utf8
   database: testapp_test
   username: root
   password: password
   host: localhost
production:
   adapter: mysql
   encoding: utf8
   database: testapp_production
   username: root
   password: password
   host: localhost

生成迁移

假设您有一个包含书籍的表,包括其标题、价格和简短描述。以下迁移设置此表 −

testapp > ruby​​ script/generate migration books

现在按如下方式修改 testapp/db/migrate/20080616170315_books.rb 文件 −

class Books < ActiveRecord::Migration
   def self.up
      create_table :books do |t|
         t.string     :title, :limit => 32, :null => false
         t.float      :price
         t.text       :description
         t.timestamp  :created_at
      end
   end
  
   def self.down
      drop_table :books
   end
end

现在按如下方式运行迁移 −

testapp > rake db:migrate

这将在 testapp_development 数据库中创建 books 表。此后,我们需要使用 rake 命令设置您的测试数据库,如下所示 −

C:
uby estapp > rake db:test:clone_structure

这将把 testapp_development 数据库克隆到 testapp_test 数据库中。这意味着无论您在开发数据库中拥有什么,现在您在测试数据库中也会拥有相同的数据。

测试模型

当您使用生成脚本生成 model 时,Rails 还会在测试目录中为该模型生成一个单元测试脚本。它还会创建一个 fixture,即一个 YAML 文件,其中包含要加载到 testapp_test 数据库中的测试数据。这是您的单元测试将针对的数据 −

testapp > ruby script/generate model Book
   exists  app/models/
   exists  test/unit/
   exists  test/fixtures/
   create  app/models/book.rb
   create  test/unit/book_test.rb
   create  test/fixtures/books.yml
   create  db/migrate
   create  db/migrate/20080616164236_create_books.rb

在模型类中编写代码时,您将在这些文件中编写相应的测试。因此,让我们在 test/fixtures/books.yml 中使用 YAML 创建两个测试书籍记录,如下所示 −

perl_cb:
   id: 1
   title: 'Ruby Tutorial'
   price: 102.00
   description : 'This is a nice Ruby tutorial'
java_cb:
   id: 2
   title: 'Java Programming'
   price: 62.00
   description : 'Java Programming for the beginners'

现在让我们用以下代码替换 book 单元测试文件 test/unit/book_test.rb 中的现有代码 −

require File.dirname(__FILE__) + '/../test_helper'

class BookTest < ActiveSupport::TestCase
   fixtures :books

   def test_book

      perl_book = Book.new :title => books(:perl_cb).title, 
         :price => books(:perl_cb).price,
         :description => books(:perl_cb).description,
         :created_at => books(:perl_cb).created_at

      assert perl_book.save

      perl_book_copy = Book.find(perl_book.id)

      assert_equal perl_book.title, perl_book_copy.title

      perl_book.title = "Ruby Tutorial"

      assert perl_book.save
      assert perl_book.destroy
   end
end

最后,运行测试方法如下 −

testapp > ruby​​ test/unit/book_test.rb

这是运行成功测试用例的输出 −

testapp > ruby test/unit/book_test_crud.rb 
Loaded suite ./test/unit/book_test
Started
.
Finished in 0.0625 seconds.

1 tests, 4 assertions, 0 failures, 0 errors

让我们分析一下这里发生了什么 −

  • BookTest 方法首先使用文本 fixture/books.yml 中第一条记录的标题和其他字段创建一个新的 Book 对象。生成的对象存储在 perl_book 实例变量中。

  • 第一个断言测试 Book 对象是否保存成功。

  • 接下来,使用 find 方法检索 book 对象并将其存储在另一个名为 perl_book_copy 的实例变量中。下一个断言将测试此检索是否成功,该断言将比较两个 book 对象的标题。此时,我们已经测试了创建和读取数据库记录的能力。

  • 该解决方案通过为存储在 perl_book 中的对象分配新标题来测试更新,然后断言保存更改成功。

  • 最后,测试销毁 Book 对象的能力。

这就是我们测试 Rails 模型的方法。

测试控制器

控制器测试也称为功能测试。功能测试测试控制器的以下功能类型 −

  • 响应是否按预期重定向?
  • 是否呈现了预期的模板?
  • 路由是否按预期进行?
  • 响应是否包含预期的标签?

Rails 框架支持五种类型的请求 −

  • get
  • post
  • put
  • head
  • delete

要编写功能测试,您需要模拟控制器将处理的五种 HTTP 请求类型中的任何一种。

请求类型"get"和"post"是控制器测试中最常用的。所有这些方法都接受四个参数 −

  • 控制器的操作
  • 请求参数的可选哈希值
  • 可选会话哈希值
  • 可选闪存哈希值

在本教程中,我们将了解如何使用 get 方法来测试我们的控制器。您可以以类似的方式测试其余方法。

当您使用 generate 生成 控制器 时,Rails 会为控制器创建一个功能测试脚本,如下所示 −

testapp > ruby script/generate controller Book
   exists  app/controllers/
   exists  app/helpers/
   create  app/views/book
   exists  test/functional/
   create  app/controllers/book_controller.rb
   create  test/functional/book_controller_test.rb
   create  app/helpers/book_helper.rb

在控制器类中编写代码时,您将在这些文件中编写相应的测试。在此之前,让我们在 app/controllers/book_controller.rb 中定义控制器函数 list、showsearch,如下所示 −

class BookController < ApplicationController
   def list
      @book_pages, @books = paginate :books, :per_page => 10
   end

   def show
      @book = Book.find(params[:id])
   end

   def search
      @book = Book.find_by_title(params[:title])
      if @book
         redirect_to :action => 'show', :id => @book.id
      else    
         flash[:error] = 'No such book available'
         redirect_to :action => 'list'
      end
   end
end

注意 − 您需要两个用于 showlist 方法的视图模板。您可以定义这些视图并对其进行测试,但现在,我们将在不定义这些视图的情况下继续进行。

现在让我们重用 test/fixtures/books.yml 文件中的测试装置,如下所示 −

perl_cb:
   id: 1
   title: 'Ruby Tutorial'
   price: 102.00
   description : 'This is a nice Ruby tutorial'
java_cb:
  id: 2
  title: 'Java Programming'
  price: 62.00
  description : 'Java Programming for the beginners'

将以下 test_search_booktest_search_not_found 方法添加到 test/ functional/book_controller_test.rb 以测试 Book Controller 搜索操作的功能。

require File.dirname(__FILE__) + '/../test_helper'
require 'book_controller'

# 重新引发控制器捕获的错误。
class BookController
   def rescue_action(e) 
      raise e 
   end
end

class BookControllerTest < Test::Unit::TestCase
   fixtures :books
   def setup
      @controller = BookController.new
      @request    = ActionController::TestRequest.new
      @response   = ActionController::TestResponse.new
   end

   def test_search_book
      get :search, :title => 'Ruby Tutorial'
      assert_not_nil assigns(:book)
      assert_equal books(:perl_cb).title, assigns(:book).title
      assert_valid assigns(:book)
      assert_redirected_to :action => 'show'
   end

   def test_search_not_found
      get :search, :title => 'HTML Tutorial'
      assert_redirected_to :action => 'list'
      assert_equal 'No such book available', flash[:error]
   end
end

现在按如下方式运行测试用例 −

testapp > ruby​​ test/ functional/book_controller_test.rb

它给出以下输出 −

Loaded suite test/functional/book_controller_test
Started
..
Finished in 0.422 seconds.

2 tests, 7 assertions, 0 failures, 0 errors

让我们分析一下这里发生了什么 −

  • setup 方法是创建控制器、请求和响应对象的默认方法。它们将由 Rails 内部使用。

  • 第一个测试方法 test_search_book 生成对搜索操作的 get 请求,并传入 title 参数。

  • 接下来的两个断言验证 Book 对象是否保存在名为 @book 的实例变量中,以及该对象是否通过了可能存在的任何 Active Record 验证。

  • 第一个方法中的最后一个断言测试请求是否重定向到控制器的 show 操作。

  • 第二个测试方法 test_search_not_found 执行另一个 get 请求,但传入了无效的标题

  • 第一个断言测试是否发出了到 list 操作的重定向。

  • 如果继续断言通过后,flash 哈希中应该有一条消息,您可以使用 assert_equal 进行测试。

要获取有关断言的更多信息,请参阅 Rails 标准文档

使用 Rake 进行测试

您可以使用 rake 实用程序来测试您的应用程序。下面列出了一些重要的命令。

  • $rake test − 测试所有单元测试和功能测试(以及集成测试,如果存在)。

  • $rake test: functionals−运行所有功能测试。

  • $rake test:units − 运行所有单元测试。

  • $rake test:integration − 运行所有集成测试。

  • $rake test:plugins − 运行 ./vendor/plugins/**/test 中的所有测试。

  • $rake test:recent − 针对过去 10 分钟内修改过的模型和控制器运行测试 −

  • $rake test:uncommitted − 对于 Subversion 中的项目,针对自上次提交以来模型和控制器中发生的更改运行测试−