25、Rails 应用程序测试指南

Rails 应用程序测试指南

在开发 Rails 应用程序时,测试是确保代码质量和功能正确性的重要环节。本文将详细介绍如何对 Rails 应用程序进行单元测试和功能测试。

单元测试

Rails 会自动生成一些测试文件,我们可以从这些文件开始进行单元测试。以 Article 模型的测试为例,打开 test/model/article_test.rb 文件:

require 'test_helper'

class ArticleTest < ActiveSupport::TestCase
  # test "the truth" do
  #  assert true
  # end
end

这个测试文件虽然简单,但为我们提供了一个构建实际测试的模板。它包含以下几个关键元素:
- 测试类 :是 ActiveSupport::TestCase 的子类, ActiveSupport::TestCase 是 Rails 对 Ruby 内置测试框架 Test::Unit 的增强版本。
- 测试方法 :使用 test 方法实现,第一个参数是测试的描述。
- 断言 :在测试用例中,使用断言来测试预期结果。

test/model 目录下,每个模型都有一个类似的测试文件。我们可以使用 rake test:models 命令来运行单元测试:

$ rake test:models
Run options: --seed 45237
# Running tests:

Finished tests in 0.020474s, 0.0000 tests/s, 0.0000 assertions/s.

0 tests, 0 assertions, 0 failures, 0 errors, 0 skips

由于生成的测试用例被注释掉了,所以目前没有实际的测试运行。如果测试通过,会显示一个 . ;如果测试用例出错,会显示 E ;如果断言返回 false ,会显示 F ;测试套件完成后,会打印一个总结信息。

测试 Article 模型

我们可以从基本的 CRUD 操作开始测试 Article 模型,具体包括:
- 创建新文章
- 查找文章
- 更新文章
- 删除文章

在开始测试之前,我们需要创建一些测试数据的固定装置(fixtures)。打开 test/fixtures/articles.yml 文件,将其内容替换为:

welcome_to_rails:
  user: eugene
  title: "Welcome to Rails"
  body: "Rails is such a nice web framework written in Ruby"
  published_at: <%= 3.days.ago %>

固定装置中的数据会在测试运行之前自动插入到测试数据库中。

添加创建测试

打开 test/models/article_test.rb 文件,删除 test "the truth" 方法,并替换为 test "should create article" 方法:

require 'test_helper'

class ArticleTest < ActiveSupport::TestCase
  test "should create article" do
    article = Article.new
    article.user  = users(:eugene)
    article.title = "Test article"
    article.body  = "Test body"

    assert article.save
  end
end

这个测试用例模拟了从控制台创建新文章的过程,最后使用 assert article.save 断言文章是否成功保存。

断言测试

断言是对预期结果的声明。 Test::Unit 提供了一系列内置的断言,Rails 也添加了一些自定义的断言。以下是 Test::Unit 的标准断言列表:
- assert(boolean, message=nil)
- assert_block(message="assert_block failed.") do ... end
- assert_equal(expected, actual, message=nil)
- assert_in_delta(expected_float, actual_float, delta, message="")
- assert_instance_of(klass, object, message="")
- assert_kind_of(klass, object, message="")
- assert_match(pattern, string, message="")
- assert_nil(object, message="")
- assert_no_match(regexp, string, message="")
- assert_not_equal(expected, actual, message="")
- assert_not_nil(object, message="")
- assert_not_same(expected, actual, message="")
- assert_nothing_raised(*args) do ... end
- assert_nothing_thrown(message="") do ... end
- assert_operator(object1, operator, object2, message="")
- assert_raise(expected_exception_klass, message="") do ... end
- assert_respond_to(object, method, message="")
- assert_same(expected, actual, message="")
- assert_send(send_array, message="")
- assert_throws(expected_symbol, message="") do ... end

test "should create article" 测试用例中,使用的 assert article.save 是最基本的断言之一,它断言 article.save 的返回值为 true

运行测试:

$ rake test:models
Started
.
Finished in 0.168879 seconds.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

测试通过,说明文章创建功能正常。

添加查找测试

test/models/article_test.rb 文件中添加 test "should find article" 方法:

require 'test_helper'
class ArticleTest < ActiveSupport::TestCase
  test "should create article" do
    article = Article.new
    article.user  = users(:eugene)
    article.title = "Test article"
    article.body  = "Test body"
    assert article.save
  end

  test "should find article" do
    article_id = articles(:welcome_to_rails).id
    assert_nothing_raised { Article.find(article_id) }
  end
end

这个测试用例从固定装置中获取文章的 id ,然后使用 Article.find 方法查找文章。使用 assert_nothing_raised 断言查找过程中不会抛出异常。

运行测试:

$ rake test:models
Run options: --seed 36921

# Running tests:

...

Finished tests in 0.068310s, 29.2781 tests/s, 14.6391 assertions/s.

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

测试通过,说明文章查找功能正常。

添加更新测试

test/models/article_test.rb 文件中添加 test "should update article" 方法:

require 'test_helper'

class ArticleTest < ActiveSupport::TestCase
  test "should create article" do
    article = Article.new
    article.user  = users(:eugene)
    article.title = "Test article"
    article.body  = "Test body"
    assert article.save
  end

  test "should find article" do
    article_id = articles(:welcome_to_rails).id
    assert_nothing_raised { Article.find(article_id) }
  end

  test "should update article" do
    article = articles(:welcome_to_rails)
    assert article.update_attributes(:title => 'New title')
  end
end

这个测试用例从固定装置中获取文章,然后使用 update_attributes 方法更新文章的标题。使用 assert 断言更新操作成功。

运行测试:

$ rake test:models
Run options: --seed 2804

# Running tests:

...

Finished tests in 0.073883s, 40.6050 tests/s, 27.0700 assertions/s.

3 tests, 2 assertions, 0 failures, 0 errors, 0 skips

测试通过,说明文章更新功能正常。

添加删除测试

test/models/article_test.rb 文件中添加 test "should destroy article" 方法:

require 'test_helper'

class ArticleTest < ActiveSupport::TestCase
  test "should create article" do
    article = Article.new
    article.user  = users(:eugene)
    article.title = "Test article"
    article.body  = "Test body"
    assert article.save
  end

  test "should find article" do
    article_id = articles(:welcome_to_rails).id
    assert_nothing_raised { Article.find(article_id) }
  end

  test "should update article" do
    article = articles(:welcome_to_rails)
    assert article.update_attributes(:title => 'New title')
  end

  test "should destroy article" do
    article = articles(:welcome_to_rails)
    article.destroy
    assert_raise(ActiveRecord::RecordNotFound) { Article.find(article.id) }
  end
end

这个测试用例从固定装置中获取文章,然后使用 destroy 方法删除文章。使用 assert_raise 断言在尝试查找已删除的文章时会抛出 ActiveRecord::RecordNotFound 异常。

运行测试:

$ rake test:models
Run options: --seed 64949

# Running tests:

....

Finished tests in 0.078197s, 51.1526 tests/s, 38.3644 assertions/s.

4 tests, 3 assertions, 0 failures, 0 errors, 0 skips

测试通过,说明文章删除功能正常。

测试验证

Article 模型有一些验证规则,例如标题和正文必须存在。我们可以添加一个测试用例来验证这些规则:

require 'test_helper'

class ArticleTest < ActiveSupport::TestCase
  test "should create article" do
    article = Article.new
    article.user  = users(:eugene)
    article.title = "Test article"
    article.body  = "Test body"
    assert article.save
  end

  test "should find article" do
    article_id = articles(:welcome_to_rails).id
    assert_nothing_raised { Article.find(article_id) }
  end

  test "should update article" do
    article = articles(:welcome_to_rails)
    assert article.update_attributes(:title => 'New title')
  end

  test "should destroy article" do
    article = articles(:welcome_to_rails)
    article.destroy
    assert_raise(ActiveRecord::RecordNotFound) { Article.find(article.id) }
  end

  test "should not create an article without title nor body" do
    article = Article.new
    assert !article.valid?
    assert article.errors[:title].any?
    assert article.errors[:body].any?
    assert_equal ["can't be blank"], article.errors[:title]
    assert_equal ["can't be blank"], article.errors[:body]
    assert !article.save
  end
end

这个测试用例创建一个没有标题和正文的 Article 对象,然后使用一系列断言来验证该对象无效,并且错误信息符合预期。

运行测试:

$ rake test:models
Run options: --seed 49599

# Running tests:

.....

Finished tests in 0.108590s, 46.0446 tests/s, 82.8802 assertions/s.

5 tests, 9 assertions, 0 failures, 0 errors, 0 skips

测试通过,说明验证规则正常工作。

当需求发生变化时,我们可能需要更新测试用例。建议先更新测试用例(这会使测试失败),然后更新代码(使测试通过),这就是测试驱动开发(TDD)。

功能测试

功能测试用于检查控制器的功能。与单元测试不同,功能测试在 Web 应用程序的上下文中进行,涉及 Web 请求和响应以及 URL。

测试 Articles 控制器

打开 test/functional/articles_controller_test.rb 文件:

require 'test_helper'

class ArticlesControllerTest < ActionController::TestCase
  # ...
end

与单元测试一样,首先需要引入 test_helper 文件。 ArticlesControllerTest ActionController::TestCase 的子类,它会自动创建 @controller @request @response 三个实例变量,用于模拟控制器环境。

在测试之前,我们需要创建一个测试辅助方法来模拟用户登录。打开 test/test_helper.rb 文件,添加 login_as 方法:

ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'rails/test_help'

class ActiveSupport::TestCase
  ActiveRecord::Migration.check_pending!

  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  #
  # Note: You'll currently still have to declare fixtures explicitly in integration tests
  # -- they do not yet inherit this setting
  fixtures :all

  # Add more helper methods to be used by all tests here...
  def login_as(user)
    @request.session[:user_id] = users(user).id
  end
end

login_as 方法将用户的 id 设置到 @request.session 中,模拟用户登录状态。

测试索引动作

更新 test/controllers/articles_controller_test.rb 文件中的 setup 方法和 test "should get index" 方法:

require 'test_helper'

class ArticlesControllerTest < ActionController::TestCase
  setup do
    @article = articles(:welcome_to_rails)
  end

  test "should get index" do
    get :index
    assert_response :success
    assert_template 'index'
    assert_not_nil assigns(:articles)
  end

  test "should get new" do
    get :new
    assert_response :success
  end

  test "should create article" do
    assert_difference('Article.count') do
      post :create, :article => @article.attributes
    end
    assert_redirected_to article_path(assigns(:article))
  end

  test "should show article" do
    get :show, :id => @article.to_param
    assert_response :success
  end

  test "should get edit" do
    get :edit, :id => @article.to_param
    assert_response :success
  end

  test "should update article" do
    put :update, :id => @article.to_param, :article => @article.attributes
    assert_redirected_to article_path(assigns(:article))
  end

  test "should destroy article" do
    assert_difference('Article.count', -1) do
      delete :destroy, :id => @article.to_param
    end

    assert_redirected_to articles_path
  end
end

setup 方法在每个测试用例之前执行,将 :welcome_to_rails 记录从固定装置中赋值给 @article 实例变量。

test "should get index" 方法使用 get :index 发送一个 GET 请求到 index 动作,然后使用以下断言来验证响应:
- assert_response :success :断言响应成功。
- assert_template 'index' :断言渲染的模板是 index
- assert_not_nil assigns(:articles) :断言 assigns(:articles) 不为 nil

assert_response 是 Rails 自定义的断言,用于验证响应的状态码。以下是 assert_response 可用的状态码快捷方式:
| 符号 | 含义 |
| ---- | ---- |
| :success | 状态码为 200 |
| :redirect | 状态码在 300 - 399 范围内 |
| :missing | 状态码为 404 |
| :error | 状态码在 500 - 599 范围内 |

通过以上单元测试和功能测试,我们可以确保 Article 模型和 Articles 控制器的功能正常工作。在开发过程中,不断编写和更新测试用例可以帮助我们及时发现和解决问题,提高代码的质量和可维护性。

Rails 应用程序测试指南

后续测试及注意事项

在完成了对 Article 模型和 Articles 控制器的基本测试后,我们还可以进一步拓展测试范围,以确保应用程序的稳定性和可靠性。

异常处理测试

除了对正常流程进行测试,我们还需要对异常情况进行测试。例如,在查找文章时,如果传入的 id 不存在,应该返回相应的错误信息。我们可以在 ArticleTest 类中添加如下测试方法:

require 'test_helper'

class ArticleTest < ActiveSupport::TestCase
  # 之前的测试方法...

  test "should raise error when finding non-existent article" do
    non_existent_id = 9999
    assert_raise(ActiveRecord::RecordNotFound) { Article.find(non_existent_id) }
  end
end

运行测试:

$ rake test:models
Run options: --seed [随机种子]

# Running tests:

......

Finished tests in [时间]s, [测试速度] tests/s, [断言速度] assertions/s.

[测试数量] tests, [断言数量] assertions, 0 failures, 0 errors, 0 skips

这个测试用例使用 assert_raise 断言当查找一个不存在的文章 id 时,会抛出 ActiveRecord::RecordNotFound 异常。

边界条件测试

对于 Article 模型的验证规则,我们还可以进行边界条件测试。例如,标题和正文的长度限制。假设我们在模型中添加了长度限制:

class Article < ApplicationRecord
  validates :title, presence: true, length: { minimum: 5, maximum: 100 }
  validates :body, presence: true, length: { minimum: 10, maximum: 1000 }
end

我们可以添加如下测试用例:

require 'test_helper'

class ArticleTest < ActiveSupport::TestCase
  # 之前的测试方法...

  test "should not create article with short title" do
    article = Article.new
    article.user = users(:eugene)
    article.title = "abc"
    article.body = "This is a test body."
    assert !article.valid?
    assert article.errors[:title].any?
    assert_equal ["is too short (minimum is 5 characters)"], article.errors[:title]
    assert !article.save
  end

  test "should not create article with long title" do
    article = Article.new
    article.user = users(:eugene)
    article.title = "a" * 101
    article.body = "This is a test body."
    assert !article.valid?
    assert article.errors[:title].any?
    assert_equal ["is too long (maximum is 100 characters)"], article.errors[:title]
    assert !article.save
  end

  test "should not create article with short body" do
    article = Article.new
    article.user = users(:eugene)
    article.title = "Test title"
    article.body = "abc"
    assert !article.valid?
    assert article.errors[:body].any?
    assert_equal ["is too short (minimum is 10 characters)"], article.errors[:body]
    assert !article.save
  end

  test "should not create article with long body" do
    article = Article.new
    article.user = users(:eugene)
    article.title = "Test title"
    article.body = "a" * 1001
    assert !article.valid?
    assert article.errors[:body].any?
    assert_equal ["is too long (maximum is 1000 characters)"], article.errors[:body]
    assert !article.save
  end
end

这些测试用例分别测试了标题和正文长度过短和过长的情况,确保验证规则在边界条件下也能正常工作。

测试流程总结

为了更清晰地展示测试的流程,我们可以使用 mermaid 流程图:

graph LR
    A[开始] --> B[创建测试数据固定装置]
    B --> C[编写单元测试用例]
    C --> D[运行单元测试]
    D --> E{单元测试是否通过}
    E -- 是 --> F[编写功能测试用例]
    E -- 否 --> C
    F --> G[运行功能测试]
    G --> H{功能测试是否通过}
    H -- 是 --> I[完成测试]
    H -- 否 --> F
持续集成与测试

在实际开发中,我们可以将测试集成到持续集成(CI)流程中。例如,使用 Travis CI 或 GitLab CI/CD 等工具,每次代码提交或合并时自动运行测试。以下是一个简单的 .travis.yml 配置示例:

language: ruby
rvm:
  - 2.7.6
before_script:
  - bundle install
  - rake db:create
  - rake db:migrate RAILS_ENV=test
script:
  - rake test

这个配置文件指定了 Ruby 版本,在测试前安装依赖、创建数据库和迁移,然后运行测试。

总结

通过对 Article 模型和 Articles 控制器的单元测试和功能测试,我们可以全面地验证应用程序的功能。在测试过程中,我们需要考虑正常流程、异常情况和边界条件,确保应用程序的稳定性和可靠性。同时,将测试集成到持续集成流程中,可以及时发现代码变更带来的问题,提高开发效率和代码质量。

在未来的开发中,我们还可以进一步拓展测试范围,例如进行集成测试、性能测试和安全测试等,以确保应用程序在各种场景下都能正常工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值