更多内容请访问 rubyonrails.org:

测试 Rails 应用程序

本指南探讨了如何在 Rails 中编写测试。

阅读本指南后,您将了解

  • Rails 测试术语。
  • 如何为您的应用程序编写单元、功能、集成和系统测试。
  • 其他流行的测试方法和插件。

1. 为什么编写测试?

编写自动化测试比通过浏览器或控制台进行手动测试能更快地确保您的代码按预期运行。失败的测试可以迅速发现问题,让您在开发过程的早期识别并修复错误。这种做法不仅提高了代码的可靠性,也增加了您对更改的信心。

Rails 使得编写测试变得容易。您可以在下一节中阅读更多关于 Rails 内置测试支持的内容。

2. 测试简介

在 Rails 中,测试从创建新应用程序之初就处于开发过程的核心。

2.1. 测试设置

一旦您使用 bin/rails new application_name 创建 Rails 项目,Rails 就会为您创建一个 test 目录。如果您列出该目录的内容,您将看到

$ ls -F test
controllers/                     helpers/                         mailers/                         fixtures/                        integration/                     models/                          test_helper.rb

2.2. 测试目录

helpersmailersmodels 目录分别存储 视图帮助方法邮件程序模型 的测试。

controllers 目录用于 与控制器相关的测试、路由和视图,其中将模拟 HTTP 请求并对结果进行断言。

integration 目录保留用于 涵盖控制器之间交互的测试

system 测试目录包含 系统测试,用于对您的应用程序进行完整的浏览器测试。系统测试允许您以用户体验的方式测试您的应用程序,并帮助您测试 JavaScript。系统测试继承自 Capybara,并在浏览器中执行应用程序测试。

固定数据 (Fixtures) 是一种模拟数据以用于测试的方式,这样您就不必使用“真实”数据。它们存储在 fixtures 目录中,您可以在下面的 固定数据 部分阅读更多相关内容。

当您首次生成作业时,还会为您的作业测试创建一个 jobs 目录。

test_helper.rb 文件保存您的测试的默认配置。

application_system_test_case.rb 保存您的系统测试的默认配置。

2.3. 测试环境

默认情况下,每个 Rails 应用程序都有三个环境:开发、测试和生产。

每个环境的配置都可以类似地修改。在这种情况下,我们可以通过更改 config/environments/test.rb 中的选项来修改我们的测试环境。

您的测试在 RAILS_ENV=test 下运行。这由 Rails 自动设置。

2.4. 编写第一个测试

我们在 Rails 入门 指南中介绍了 bin/rails generate model 命令。除了创建模型,此命令还会在 test 目录中创建测试存根

$ bin/rails generate model article title:string body:text
...
create  app/models/article.rb
create  test/models/article_test.rb
...

test/models/article_test.rb 中的默认测试存根如下所示

require "test_helper"

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

逐行检查此文件将有助于您熟悉 Rails 测试代码和术语。

require "test_helper"

引入 test_helper.rb 文件会加载运行测试的默认配置。添加到此文件的所有方法在此文件包含时也可用。

class ArticleTest < ActiveSupport::TestCase
  # ...
end

这称为测试用例,因为 ArticleTest 类继承自 ActiveSupport::TestCase。因此,它也拥有 ActiveSupport::TestCase 中的所有可用方法。在本指南的后面部分,我们将看到它提供的一些方法。

任何在继承自 Minitest::Test(它是 ActiveSupport::TestCase 的超类)的类中定义且以 test_ 开头的方法都简称为测试。因此,定义为 test_passwordtest_valid_password 的方法是测试名称,并在运行测试用例时自动运行。

Rails 还添加了一个 test 方法,该方法接受测试名称和一个块。它生成一个标准的 Minitest::Unit 测试,方法名称前缀为 test_,让您可以专注于编写测试逻辑,而不必考虑方法命名。例如,您可以编写

test "the truth" do
  assert true
end

这大约等同于编写此代码

def test_the_truth
  assert true
end

尽管您仍然可以使用常规方法定义,但使用 test 宏可以实现更具可读性的测试名称。

方法名称是通过将空格替换为下划线生成的。结果不必是有效的 Ruby 标识符,因为 Ruby 允许任何字符串作为方法名称,包括包含标点符号的字符串。尽管这可能需要使用 define_methodsend 来定义和调用此类方法,但对名称本身几乎没有正式限制。

测试的这部分称为“断言”

assert true

断言是评估对象(或表达式)以获取预期结果的代码行。例如,断言可以检查

  • 此值是否等于彼值?
  • 此对象是否为 nil?
  • 这行代码是否抛出异常?
  • 用户的密码是否大于 5 个字符?

每个测试可以包含一个或多个断言,断言数量没有限制。只有当所有断言都成功时,测试才会通过。

2.4.1. 您的第一个失败测试

要查看测试失败的报告方式,您可以向 article_test.rb 测试用例添加一个失败的测试。在此示例中,断言文章在不满足某些条件的情况下将无法保存;因此,如果文章成功保存,测试将失败,从而演示测试失败。

require "test_helper"

class ArticleTest < ActiveSupport::TestCase
  test "should not save article without title" do
    article = Article.new
    assert_not article.save
  end
end

这是运行此新添加的测试的输出

$ bin/rails test test/models/article_test.rb
Running 1 tests in a single process (parallelization threshold is 50)
Run options: --seed 44656

# Running:

F

Failure:
ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:4]:
Expected true to be nil or false


bin/rails test test/models/article_test.rb:4



Finished in 0.023918s, 41.8090 runs/s, 41.8090 assertions/s.

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

在输出中,F 表示测试失败。Failure 部分包含失败测试的名称,后跟堆栈跟踪和一条消息,显示断言的实际值和预期值。默认断言消息提供足够的信息来帮助识别错误。为了提高可读性,每个断言都允许一个可选的消息参数来自定义失败消息,如下所示

test "should not save article without title" do
  article = Article.new
  assert_not article.save, "Saved the article without a title"
end

运行此测试会显示更友好的断言消息

Failure:
ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]:
Saved the article without a title

要使此测试通过,可以为 title 字段添加模型级验证。

class Article < ApplicationRecord
  validates :title, presence: true
end

现在测试应该通过,因为我们测试中的文章没有用 title 初始化,所以模型验证将阻止保存。这可以通过再次运行测试来验证

$ bin/rails test test/models/article_test.rb:6
Running 1 tests in a single process (parallelization threshold is 50)
Run options: --seed 31252

# Running:

.

Finished in 0.027476s, 36.3952 runs/s, 36.3952 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

显示的小绿点表示测试已成功通过。

在上述过程中,首先编写一个针对所需功能的失败测试,然后编写一些添加该功能的代码。最后,再次运行测试以确保其通过。这种软件开发方法被称为 测试驱动开发 (TDD)。

2.4.2. 报告错误

要查看错误报告方式,这里是一个包含错误的测试

test "should report error" do
  # some_undefined_variable is not defined elsewhere in the test case
  some_undefined_variable
  assert true
end

现在您可以在控制台中看到更多运行测试的输出

$ bin/rails test test/models/article_test.rb
Running 2 tests in a single process (parallelization threshold is 50)
Run options: --seed 1808

# Running:

E

Error:
ArticleTest#test_should_report_error:
NameError: undefined local variable or method 'some_undefined_variable' for #<ArticleTest:0x007fee3aa71798>
    test/models/article_test.rb:11:in 'block in <class:ArticleTest>'


bin/rails test test/models/article_test.rb:9

.

Finished in 0.040609s, 49.2500 runs/s, 24.6250 assertions/s.

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

请注意输出中的“E”。它表示一个有错误的测试。上面“Finished”行的小绿点表示一个通过的测试。

一旦遇到任何错误或断言失败,每个测试方法的执行就会停止,测试套件会继续下一个方法。所有测试方法都以随机顺序执行。config.active_support.test_order 选项可用于配置测试顺序。

当测试失败时,您会看到相应的回溯。默认情况下,Rails 会过滤回溯,并且只打印与您的应用程序相关的行。这消除了噪音,并帮助您专注于代码。但是,在您想要查看完整回溯的情况下,请设置 -b (或 --backtrace) 参数以启用此行为

$ bin/rails test -b test/models/article_test.rb

如果您希望此测试通过,您可以修改它以使用 assert_raises (这样您现在正在检查是否存在错误),如下所示

test "should report error" do
  # some_undefined_variable is not defined elsewhere in the test case
  assert_raises(NameError) do
    some_undefined_variable
  end
end

此测试现在应该通过。

2.5. Minitest 断言

到目前为止,您已经瞥见了某些可用的断言。断言是测试的基础块。它们是实际执行检查以确保事情按计划进行的那些。

以下是您可以与 minitest(Rails 使用的默认测试库)一起使用的一些断言摘录。[msg] 参数是您可以指定的可选字符串消息,以使您的测试失败消息更清晰。

断言 用途
assert(test, [msg]) 确保 test 为真。
assert_not(test, [msg]) 确保 test 为假。
assert_equal(expected, actual, [msg]) 确保 expected == actual 为真。
assert_not_equal(expected, actual, [msg]) 确保 expected != actual 为真。
assert_same(expected, actual, [msg]) 确保 expected.equal?(actual) 为真。
assert_not_same(expected, actual, [msg]) 确保 expected.equal?(actual) 为假。
assert_nil(obj, [msg]) 确保 obj.nil? 为真。
assert_not_nil(obj, [msg]) 确保 obj.nil? 为假。
assert_empty(obj, [msg]) 确保 objempty?
assert_not_empty(obj, [msg]) 确保 obj 不为 empty?
assert_match(regexp, string, [msg]) 确保字符串与正则表达式匹配。
assert_no_match(regexp, string, [msg]) 确保字符串不与正则表达式匹配。
assert_includes(collection, obj, [msg]) 确保 objcollection 中。
assert_not_includes(collection, obj, [msg]) 确保 obj 不在 collection 中。
assert_in_delta(expected, actual, [delta], [msg]) 确保数字 expectedactualdelta 范围内。
assert_not_in_delta(expected, actual, [delta], [msg]) 确保数字 expectedactual 不在 delta 范围内。
assert_in_epsilon(expected, actual, [epsilon], [msg]) 确保数字 expectedactual 的相对误差小于 epsilon
assert_not_in_epsilon(expected, actual, [epsilon], [msg]) 确保数字 expectedactual 的相对误差不小于 epsilon
assert_throws(symbol, [msg]) { block } 确保给定的块抛出符号。
assert_raises(exception1, exception2, ...) { block } 确保给定的块抛出给定的异常之一。
assert_instance_of(class, obj, [msg]) 确保 objclass 的实例。
assert_not_instance_of(class, obj, [msg]) 确保 obj 不是 class 的实例。
assert_kind_of(class, obj, [msg]) 确保 objclass 的实例或其子类。
assert_not_kind_of(class, obj, [msg]) 确保 obj 不是 class 的实例且不是其子类。
assert_respond_to(obj, symbol, [msg]) 确保 obj 响应 symbol
assert_not_respond_to(obj, symbol, [msg]) 确保 obj 不响应 symbol
assert_operator(obj1, operator, [obj2], [msg]) 确保 obj1.operator(obj2) 为真。
assert_not_operator(obj1, operator, [obj2], [msg]) 确保 obj1.operator(obj2) 为假。
assert_predicate(obj, predicate, [msg]) 确保 obj.predicate 为真,例如 assert_predicate str, :empty?
assert_not_predicate(obj, predicate, [msg]) 确保 obj.predicate 为假,例如 assert_not_predicate str, :empty?
flunk([msg]) 确保失败。这对于显式标记尚未完成的测试很有用。

以上是 minitest 支持的断言的一个子集。有关详尽且最新的列表,请查看 minitest API 文档,特别是 Minitest::Assertions

使用 minitest,您可以添加自己的断言。事实上,Rails 正是这样做的。它包含一些专门的断言,让您的生活更轻松。

创建您自己的断言是一个我们不会在本指南中深入探讨的主题。

2.6. Rails 特有的断言

Rails 向 minitest 框架添加了一些自己的自定义断言

断言 用途
assert_difference(expressions, difference = 1, message = nil) {...} 测试在所提供的块中评估后表达式返回值之间的数值差异。
assert_no_difference(expressions, message = nil, &block) 断言在调用传入块之前和之后评估表达式的数值结果没有改变。
assert_changes(expressions, message = nil, from:, to:, &block) 测试在调用传入块之后评估表达式的结果是否改变。
assert_no_changes(expressions, message = nil, &block) 测试在调用传入块之后评估表达式的结果是否没有改变。
assert_nothing_raised { block } 确保给定的块不抛出任何异常。
assert_recognizes(expected_options, path, extras = {}, message = nil) 断言给定路径的路由处理正确,并且解析的选项(在 expected_options hash 中给出)与路径匹配。基本上,它断言 Rails 识别 expected_options 给出的路由。
assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil) 断言提供的选项可以用来生成提供的路径。这是 assert_recognizes 的反向操作。extra 参数用于告诉请求查询字符串中可能存在的额外请求参数的名称和值。message 参数允许您为断言失败指定自定义错误消息。
assert_routing(expected_path, options, defaults = {}, extras = {}, message = nil) 断言 pathoptions 双向匹配;换句话说,它验证 path 生成 options,然后 options 生成 path。这实质上将 assert_recognizesassert_generates 合并为一步。extras 哈希允许您指定通常作为查询字符串提供给操作的选项。message 参数允许您指定自定义错误消息以在失败时显示。
assert_response(type, message = nil) 断言响应具有特定的状态码。您可以指定 :success 表示 200-299,:redirect 表示 300-399,:missing 表示 404,或者 :error 匹配 500-599 范围。您还可以传递一个显式的状态码或其符号等效项。有关更多信息,请参阅状态码完整列表以及它们的映射工作方式。
assert_redirected_to(options = {}, message = nil) 断言响应是重定向到与给定选项匹配的 URL。您还可以传递命名路由,例如 assert_redirected_to root_path 和 Active Record 对象,例如 assert_redirected_to @article
assert_queries_count(count = nil, include_schema: false, &block) 断言 &block 生成 int 数量的 SQL 查询。
assert_no_queries(include_schema: false, &block) 断言 &block 不生成任何 SQL 查询。
assert_queries_match(pattern, count: nil, include_schema: false, &block) 断言 &block 生成与模式匹配的 SQL 查询。
assert_no_queries_match(pattern, &block) 断言 &block 不生成与模式匹配的 SQL 查询。
assert_error_reported(class) { block } 断言错误类已报告,例如 assert_error_reported IOError { Rails.error.report(IOError.new("Oops")) }
assert_no_error_reported { block } 断言没有报告错误,例如 assert_no_error_reported { perform_service }

您将在下一章中看到这些断言的一些用法。

2.7. 测试用例中的断言

Minitest::Assertions 中定义的所有基本断言(例如 assert_equal)在我们的测试用例中使用的类中也可用。事实上,Rails 为您提供了以下可供继承的类

这些类都包含 Minitest::Assertions,允许我们在测试中使用所有基本断言。

有关 minitest 的更多信息,请参阅 minitest 文档

2.8. Rails 测试运行器

我们可以使用 bin/rails test 命令一次运行所有测试。

或者我们可以通过将文件名附加到 bin/rails test 命令来运行单个测试文件。

$ bin/rails test test/models/article_test.rb
Running 1 tests in a single process (parallelization threshold is 50)
Run options: --seed 1559

# Running:

..

Finished in 0.027034s, 73.9810 runs/s, 110.9715 assertions/s.

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

这将运行测试用例中的所有测试方法。

您还可以通过提供 -n--name 标志和测试的方法名来运行测试用例中的特定测试方法。

$ bin/rails test test/models/article_test.rb -n test_the_truth
Running 1 tests in a single process (parallelization threshold is 50)
Run options: -n test_the_truth --seed 43583

# Running:

.

Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.

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

您还可以通过提供行号来在特定行运行测试。

$ bin/rails test test/models/article_test.rb:6 # run specific test and line

您还可以通过提供行范围来运行一系列测试。

$ bin/rails test test/models/article_test.rb:6-20 # runs tests from line 6 to 20

您还可以通过提供目录路径来运行整个测试目录。

$ bin/rails test test/controllers # run all tests from specific directory

测试运行器还提供了许多其他功能,例如快速失败、显示详细进度等。使用以下命令查看测试运行器的文档

$ bin/rails test -h
Usage:
  bin/rails test [PATHS...]

Run tests except system tests

Examples:
    You can run a single test by appending a line number to a filename:

        bin/rails test test/models/user_test.rb:27

    You can run multiple tests with in a line range by appending the line range to a filename:

        bin/rails test test/models/user_test.rb:10-20

    You can run multiple files and directories at the same time:

        bin/rails test test/controllers test/integration/login_test.rb

    By default test failures and errors are reported inline during a run.

minitest options:
    -h, --help                       Display this help.
        --no-plugins                 Bypass minitest plugin auto-loading (or set $MT_NO_PLUGINS).
    -s, --seed SEED                  Sets random seed. Also via env. Eg: SEED=n rake
    -v, --verbose                    Verbose. Show progress processing files.
        --show-skips                 Show skipped at the end of run.
    -n, --name PATTERN               Filter run on /regexp/ or string.
        --exclude PATTERN            Exclude /regexp/ or string from run.
    -S, --skip CODES                 Skip reporting of certain types of results (eg E).

Known extensions: rails, pride
    -w, --warnings                   Run with Ruby warnings enabled
    -e, --environment ENV            Run tests in the ENV environment
    -b, --backtrace                  Show the complete backtrace
    -d, --defer-output               Output test failures and errors after the test run
    -f, --fail-fast                  Abort test run on first failure or error
    -c, --[no-]color                 Enable color in the output
        --profile [COUNT]            Enable profiling of tests and list the slowest test cases (default: 10)
    -p, --pride                      Pride. Show your testing pride!

3. 测试数据库

几乎每个 Rails 应用程序都大量与数据库交互,因此您的测试也需要一个数据库来交互。本节介绍如何设置此测试数据库并用示例数据填充它。

测试环境部分所述,每个 Rails 应用程序都有三个环境:开发、测试和生产。它们各自的数据库在 config/database.yml 中配置。

专用的测试数据库允许您独立设置和交互测试数据。这样,您的测试可以放心地与测试数据交互,而不必担心开发或生产数据库中的数据。

3.1. 维护测试数据库模式

为了运行测试,您的测试数据库需要当前的模式。测试帮助程序检查您的测试数据库是否有任何待处理的迁移。它将尝试将您的 db/schema.rbdb/structure.sql 加载到测试数据库中。如果迁移仍然待处理,将引发错误。通常这表明您的模式没有完全迁移。运行迁移(使用 bin/rails db:migrate RAILS_ENV=test)将使模式保持最新。

如果对现有迁移进行了修改,则需要重建测试数据库。这可以通过执行 bin/rails test:db 来完成。

3.2. 固定数据

对于良好的测试,您需要考虑设置测试数据。在 Rails 中,您可以通过定义和自定义固定数据来处理此问题。您可以在 固定数据 API 文档 中找到全面的文档。

3.2.1. 什么是固定数据?

固定数据 (Fixtures) 是一个花哨的词,表示一组一致的测试数据。固定数据允许您在测试运行之前用预定义的数据填充您的测试数据库。固定数据是独立于数据库的,并用 YAML 编写。每个模型有一个文件。

固定数据不是为了创建您的测试所需的所有对象而设计的,并且最好仅用于可应用于常见情况的默认数据。

固定数据存储在您的 test/fixtures 目录中。

3.2.2. YAML

YAML 是一种人类可读的数据序列化语言。YAML 格式的固定数据是一种人类友好的方式来描述您的示例数据。这些类型的固定数据具有 .yml 文件扩展名(例如 users.yml)。

这是一个示例 YAML 固定数据文件

# lo & behold! I am a YAML comment!
david:
  name: David Heinemeier Hansson
  birthday: 1979-10-15
  profession: Systems development

steve:
  name: Steve Ross Kellock
  birthday: 1974-09-27
  profession: guy with keyboard

每个固定数据都给一个名称,后跟一个缩进的冒号分隔的键/值对列表。记录通常由一个空行分隔。您可以使用第一列中的 # 字符在固定数据文件中放置注释。

如果您正在使用关联,您可以在两个不同的固定数据之间定义一个引用节点。这是一个带有 belongs_to/has_many 关联的示例

# test/fixtures/categories.yml
web_frameworks:
  name: Web Frameworks
# test/fixtures/articles.yml
first:
  title: Welcome to Rails!
  category: web_frameworks
# test/fixtures/action_text/rich_texts.yml
first_content:
  record: first (Article)
  name: content
  body: <div>Hello, from <strong>a fixture</strong></div>

注意在 fixtures/articles.yml 中找到的 first 文章的 category 键的值为 web_frameworks,以及在 fixtures/action_text/rich_texts.yml 中找到的 first_content 条目的 record 键的值为 first (Article)。这提示 Active Record 为前者加载在 fixtures/categories.yml 中找到的 Category web_frameworks,并为后者加载在 fixtures/articles.yml 中找到的 Article first

对于关联按名称相互引用,您可以使用固定数据名称而不是在关联固定数据上指定 id: 属性。Rails 将自动分配一个主键以在运行之间保持一致。有关此关联行为的更多信息,请阅读固定数据 API 文档

3.2.3. 文件附件固定数据

与其他 Active Record 支持的模型一样,Active Storage 附件记录继承自 ActiveRecord::Base 实例,因此可以通过固定数据填充。

考虑一个 Article 模型,它有一个关联的图像作为 thumbnail 附件,以及固定数据 YAML

class Article < ApplicationRecord
  has_one_attached :thumbnail
end
# test/fixtures/articles.yml
first:
  title: An Article

假设在 test/fixtures/files/first.png 处有一个 image/png 编码文件,以下 YAML 固定数据条目将生成相关的 ActiveStorage::BlobActiveStorage::Attachment 记录

# test/fixtures/active_storage/blobs.yml
first_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob filename: "first.png" %>
# test/fixtures/active_storage/attachments.yml
first_thumbnail_attachment:
  name: thumbnail
  record: first (Article)
  blob: first_thumbnail_blob

3.2.4. 在固定数据中嵌入代码

ERB 允许您在模板中嵌入 Ruby 代码。当 Rails 加载固定数据时,YAML 固定数据格式会使用 ERB 进行预处理。这允许您使用 Ruby 来帮助生成一些示例数据。例如,以下代码生成一千个用户

<% 1000.times do |n| %>
  user_<%= n %>:
    username: <%= "user#{n}" %>
    email: <%= "user#{n}@example.com" %>
<% end %>

3.2.5. 固定数据实际操作

Rails 默认自动加载 test/fixtures 目录中的所有固定数据。加载包括三个步骤

  1. 从与固定数据对应的表中删除任何现有数据
  2. 将固定数据加载到表中
  3. 将固定数据转储到方法中,以防您想直接访问它

为了从数据库中删除现有数据,Rails 尝试禁用参照完整性触发器(如外键和检查约束)。如果您在运行测试时遇到权限错误,请确保数据库用户在测试环境中具有禁用这些触发器的权限。(在 PostgreSQL 中,只有超级用户才能禁用所有触发器。在 PostgreSQL 文档中阅读更多关于权限的信息)。

3.2.6. 固定数据是 Active Record 对象

固定数据是 Active Record 的实例。如上所述,您可以直接访问对象,因为它自动作为方法可用,其作用域仅限于测试用例。例如

# this will return the User object for the fixture named david
users(:david)

# this will return the property for david called id
users(:david).id

# methods available to the User object can also be accessed
david = users(:david)
david.call(david.partner)

要一次获取多个固定数据,您可以传入固定数据名称列表。例如

# this will return an array containing the fixtures david and steve
users(:david, :steve)

3.3. 事务

默认情况下,Rails 会自动将测试包装在数据库事务中,该事务在完成后回滚。这使得测试相互独立,并意味着数据库的更改仅在单个测试中可见。

class MyTest < ActiveSupport::TestCase
  test "newly created users are active by default" do
    # Since the test is implicitly wrapped in a database transaction, the user
    # created here won't be seen by other tests.
    assert User.create.active?
  end
end

但是,ActiveRecord::Base.current_transaction 方法仍然按预期工作

class MyTest < ActiveSupport::TestCase
  test "Active Record current_transaction method works as expected" do
    # The implicit transaction around tests does not interfere with the
    # application-level semantics of the current_transaction.
    assert User.current_transaction.blank?
  end
end

如果存在多个写入数据库,则测试将包装在尽可能多的相应事务中,并且所有这些事务都将回滚。

3.3.1. 退出测试事务

单个测试用例可以选择退出

class MyTest < ActiveSupport::TestCase
  # No implicit database transaction wraps the tests in this test case.
  self.use_transactional_tests = false
end

4. 测试模型

模型测试用于测试应用程序的模型及其关联的逻辑。您可以使用我们在上面部分探讨的断言和固定数据来测试此逻辑。

Rails 模型测试存储在 test/models 目录下。Rails 提供了一个生成器为您创建模型测试骨架。

$ bin/rails generate test_unit:model article
create  test/models/article_test.rb

此命令将生成以下文件

# article_test.rb
require "test_helper"

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

模型测试没有自己的超类,例如 ActionMailer::TestCase。相反,它们继承自 ActiveSupport::TestCase

5. 控制器的功能测试

在编写功能测试时,您关注的是测试控制器操作如何处理请求以及预期的结果或响应。功能控制器测试用于测试控制器和其他行为,例如 API 响应。

5.1. 功能测试中应包含哪些内容

您可以测试以下内容

  • 网页请求是否成功?
  • 用户是否重定向到正确的页面?
  • 用户是否成功通过身份验证?
  • 响应中是否显示了正确的信息?

查看功能测试最简单的方法是使用脚手架生成器生成一个控制器

$ bin/rails generate scaffold_controller article
...
create  app/controllers/articles_controller.rb
...
invoke  test_unit
create    test/controllers/articles_controller_test.rb
...

这将为 Article 资源生成控制器代码和测试。您可以在 test/controllers 目录中查看文件 articles_controller_test.rb

如果您已经有一个控制器,并且只想为七个默认操作中的每一个生成测试脚手架代码,您可以使用以下命令

$ bin/rails generate test_unit:scaffold article
...
invoke  test_unit
create    test/controllers/articles_controller_test.rb
...

如果您正在生成测试脚手架代码,您将看到一个 @article 值被设置并在整个测试文件中使用。这个 article 实例使用 test/fixtures/articles.yml 文件中嵌套在 :one 键内的属性。在尝试运行测试之前,请确保您已在此文件中设置了键和相关值。

让我们看一下 articles_controller_test.rb 文件中的一个这样的测试,test_should_get_index

# articles_controller_test.rb
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test "should get index" do
    get articles_url
    assert_response :success
  end
end

test_should_get_index 测试中,Rails 模拟对名为 index 的操作的请求,确保请求成功,并确保已生成正确的响应正文。

get 方法启动 Web 请求并将结果填充到 @response 中。它最多可以接受 6 个参数

  • 您正在请求的控制器操作的 URI。这可以是字符串形式或路由帮助方法(例如 articles_url)。
  • params:带有请求参数哈希的选项,用于传递给操作(例如查询字符串参数或文章变量)。
  • headers:用于设置将随请求传递的头。
  • env:用于根据需要自定义请求环境。
  • xhr:请求是否为 AJAX 请求。可以设置为 true 以将请求标记为 AJAX。
  • as:用于以不同的内容类型编码请求。

所有这些关键字参数都是可选的。

示例:为第一个 Article 调用 :show 操作(通过 get 请求),传入 HTTP_REFERER

get article_url(Article.first), headers: { "HTTP_REFERER" => "http://example.com/home" }

另一个例子:为最后一个 Article 调用 :update 操作(通过 patch 请求),在 params 中传入新的 title 文本,作为 AJAX 请求

patch article_url(Article.last), params: { article: { title: "updated" } }, xhr: true

再举一个例子:调用 :create 动作(通过 post 请求)来创建新文章,在 params 中传入 title 的文本,作为 JSON 请求

post articles_url, params: { article: { title: "Ahoy!" } }, as: :json

如果您尝试运行 articles_controller_test.rb 中的 test_should_create_article 测试,它将(正确地)因新添加的模型级验证而失败。

现在修改 articles_controller_test.rb 中的 test_should_create_article 测试,以便此测试通过

test "should create article" do
  assert_difference("Article.count") do
    post articles_url, params: { article: { body: "Rails is awesome!", title: "Hello Rails" } }
  end

  assert_redirected_to article_path(Article.last)
end

您现在可以运行此测试,它将通过。

如果您遵循了基本身份验证部分中的步骤,您将需要为每个请求头添加授权才能使所有测试通过

post articles_url, params: { article: { body: "Rails is awesome!", title: "Hello Rails" } }, headers: { Authorization: ActionController::HttpAuthentication::Basic.encode_credentials("dhh", "secret") }

5.2. 功能测试的 HTTP 请求类型

如果您熟悉 HTTP 协议,您会知道 get 是一种请求类型。Rails 功能测试中支持 6 种请求类型

  • get
  • post
  • patch
  • put
  • head
  • delete

所有请求类型都有您可以使用的等效方法。在典型的 CRUD 应用程序中,您最常使用 postgetputdelete

功能测试不验证指定请求类型是否被操作接受;相反,它们关注结果。为了测试请求类型,提供了请求测试,使您的测试更具目的性。

5.3. 测试 XHR (AJAX) 请求

AJAX 请求(Asynchronous JavaScript and XML)是一种技术,通过异步 HTTP 请求从服务器获取内容,并更新页面的相关部分,而无需完全重新加载页面。

要测试 AJAX 请求,您可以为 getpostpatchputdelete 方法指定 xhr: true 选项。例如

test "AJAX request" do
  article = articles(:one)
  get article_url(article), xhr: true

  assert_equal "hello world", @response.body
  assert_equal "text/javascript", @response.media_type
end

5.4. 测试其他请求对象

任何请求发出并处理后,您将拥有 3 个可用的哈希对象

  • cookies - 设置的任何 cookie
  • flash - flash 中存在的任何对象
  • session - session 变量中存在的任何对象

与普通哈希对象一样,您可以通过引用字符串键来访问值。您也可以通过符号名称引用它们。例如

flash["gordon"]               # or flash[:gordon]
session["shmession"]          # or session[:shmession]
cookies["are_good_for_u"]     # or cookies[:are_good_for_u]

5.5. 实例变量

请求发出后,您还可以访问功能测试中的三个实例变量

  • @controller - 处理请求的控制器
  • @request - 请求对象
  • @response - 响应对象
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test "should get index" do
    get articles_url

    assert_equal "index", @controller.action_name
    assert_equal "application/x-www-form-urlencoded", @request.media_type
    assert_match "Articles", @response.body
  end
end

5.6. 设置 Header 和 CGI 变量

HTTP 头是随 HTTP 请求发送的信息片段,用于提供重要的元数据。CGI 变量是用于在 Web 服务器和应用程序之间交换信息的环境变量。

HTTP 头和 CGI 变量可以通过作为头传递进行测试

# setting an HTTP Header
get articles_url, headers: { "Content-Type": "text/plain" } # simulate the request with custom header

# setting a CGI variable
get articles_url, headers: { "HTTP_REFERER": "http://example.com/home" } # simulate the request with custom env variable

5.7. 测试 flash 通知

测试其他请求对象部分所示,测试中可访问的三个哈希对象之一是 flash。本节概述了如何在我们的博客应用程序中测试 flash 消息的出现,每当有人成功创建新文章时。

首先,应向 test_should_create_article 测试添加一个断言

test "should create article" do
  assert_difference("Article.count") do
    post articles_url, params: { article: { title: "Some title" } }
  end

  assert_redirected_to article_path(Article.last)
  assert_equal "Article was successfully created.", flash[:notice]
end

如果现在运行测试,它应该会失败

$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article
Running 1 tests in a single process (parallelization threshold is 50)
Run options: -n test_should_create_article --seed 32266

# Running:

F

Finished in 0.114870s, 8.7055 runs/s, 34.8220 assertions/s.

  1) Failure:
ArticlesControllerTest#test_should_create_article [/test/controllers/articles_controller_test.rb:16]:
--- expected
+++ actual
@@ -1 +1 @@
-"Article was successfully created."
+nil

1 runs, 4 assertions, 1 failures, 0 errors, 0 skips

现在在控制器中实现闪存消息。:create 动作应该如下所示

def create
  @article = Article.new(article_params)

  if @article.save
    flash[:notice] = "Article was successfully created."
    redirect_to @article
  else
    render "new"
  end
end

现在,如果运行测试,它们应该通过

$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article
Running 1 tests in a single process (parallelization threshold is 50)
Run options: -n test_should_create_article --seed 18981

# Running:

.

Finished in 0.081972s, 12.1993 runs/s, 48.7972 assertions/s.

1 runs, 4 assertions, 0 failures, 0 errors, 0 skips

如果您使用脚手架生成器生成了控制器,那么闪存消息将已在您的 create 动作中实现。

5.8. showupdatedelete 操作的测试

到目前为止,本指南已概述了 :index:create 操作的测试。那么其他操作呢?

您可以按如下方式编写 :show 的测试

test "should show article" do
  article = articles(:one)
  get article_url(article)
  assert_response :success
end

如果您还记得我们之前关于固定数据的讨论,articles() 方法将提供对文章固定数据的访问。

那么删除现有文章呢?

test "should delete article" do
  article = articles(:one)
  assert_difference("Article.count", -1) do
    delete article_url(article)
  end

  assert_redirected_to articles_path
end

这是更新现有文章的测试

test "should update article" do
  article = articles(:one)

  patch article_url(article), params: { article: { title: "updated" } }

  assert_redirected_to article_path(article)
  # Reload article to refresh data and assert that title is updated.
  article.reload
  assert_equal "updated", article.title
end

请注意,这三个测试存在一些重复——它们都访问相同的文章固定数据。可以通过使用 ActiveSupport::Callbacks 提供的 setupteardown 方法来 DRY(“Don't Repeat Yourself”)实现。

测试可能如下所示

require "test_helper"

class ArticlesControllerTest < ActionDispatch::IntegrationTest
  # called before every single test
  setup do
    @article = articles(:one)
  end

  # called after every single test
  teardown do
    # when controller is using cache it may be a good idea to reset it afterwards
    Rails.cache.clear
  end

  test "should show article" do
    # Reuse the @article instance variable from setup
    get article_url(@article)
    assert_response :success
  end

  test "should destroy article" do
    assert_difference("Article.count", -1) do
      delete article_url(@article)
    end

    assert_redirected_to articles_path
  end

  test "should update article" do
    patch article_url(@article), params: { article: { title: "updated" } }

    assert_redirected_to article_path(@article)
    # Reload association to fetch updated data and assert that title is updated.
    @article.reload
    assert_equal "updated", @article.title
  end
end

与其他 Rails 回调类似,setupteardown 方法也可以接受块、lambda 或作为符号的方法名来调用。

6. 集成测试

集成测试将功能控制器测试更进一步——它们侧重于测试应用程序的多个部分如何交互,通常用于测试重要的工作流程。Rails 集成测试存储在 test/integration 目录中。

Rails 提供了一个生成器来创建集成测试骨架,如下所示

$ bin/rails generate integration_test user_flows
      invoke  test_unit
      create  test/integration/user_flows_test.rb

这是一个新生成的集成测试的示例

require "test_helper"

class UserFlowsTest < ActionDispatch::IntegrationTest
  # test "the truth" do
  #   assert true
  # end
end

这里测试继承自 ActionDispatch::IntegrationTest。这使得除了标准测试帮助程序之外,还为集成测试提供了一些额外的帮助程序

6.1. 实现集成测试

让我们为我们的博客应用程序添加一个集成测试,从创建一个新的博客文章的基本工作流程开始,以验证一切是否正常运行。

首先生成集成测试骨架

$ bin/rails generate integration_test blog_flow

它应该已经创建了一个测试文件占位符。在前面的命令的输出中,您应该会看到

      invoke  test_unit
      create    test/integration/blog_flow_test.rb

现在打开该文件并编写第一个断言

require "test_helper"

class BlogFlowTest < ActionDispatch::IntegrationTest
  test "can see the welcome page" do
    get "/"
    assert_dom "h1", "Welcome#index"
  end
end

如果您访问根路径,您应该会看到渲染的 welcome/index.html.erb 视图。因此,此断言应该通过。

断言 assert_dom(别名为 assert_select)在集成测试中可用,用于检查关键 HTML 元素及其内容是否存在。

6.1.1. 创建文章集成

要测试在我们的博客中创建新文章并显示结果文章的能力,请参见下面的示例

test "can create an article" do
  get "/articles/new"
  assert_response :success

  post "/articles",
    params: { article: { title: "can create", body: "article successfully." } }
  assert_response :redirect
  follow_redirect!
  assert_response :success
  assert_dom "p", "Title:\n  can create"
end

首先调用我们的 Articles 控制器的 :new 动作,响应应该成功。

接下来,向 Articles 控制器的 :create 动作发出 post 请求

post "/articles",
  params: { article: { title: "can create", body: "article successfully." } }
assert_response :redirect
follow_redirect!

请求后的两行用于处理创建新文章时的重定向设置。

如果您计划在重定向后进行后续请求,请不要忘记调用 follow_redirect!

最后,可以断言响应成功,并且新创建的文章可以在页面上读取。

上面成功测试了访问我们的博客和创建新文章的非常小的工作流程。为了扩展此功能,可以为添加评论、编辑评论或删除文章等功能添加额外的测试。集成测试是尝试我们应用程序各种用例的好地方。

6.2. 集成测试可用的帮助方法

有许多帮助程序可用于集成测试。一些包括

7. 系统测试

与集成测试类似,系统测试允许您从用户的角度测试应用程序组件如何协同工作。它通过在真实或无头浏览器(在后台运行而无需打开可见窗口的浏览器)中运行测试来做到这一点。系统测试内部使用 Capybara

7.1. 何时使用系统测试

系统测试提供最真实的测试体验,因为它们从用户的角度测试您的应用程序。但是,它们伴随着重要的权衡

  • 它们显著慢于单元和集成测试
  • 它们可能脆弱,容易因时间问题或 UI 更改而失败
  • 随着 UI 的演变,它们需要更多维护

考虑到这些权衡,系统测试应保留用于关键用户路径,而不是为每个功能创建。考虑为以下情况编写系统测试

  • 核心业务工作流(例如,用户注册、结账流程、支付流程)
  • 集成多个组件的关键用户交互
  • 无法在较低级别测试的复杂 JavaScript 交互

对于大多数功能,集成测试在覆盖率和可维护性之间提供了更好的平衡。将系统测试保留用于需要验证完整用户体验的场景。

7.2. 生成系统测试

在使用脚手架时,Rails 默认不再生成系统测试。这一更改反映了谨慎使用系统测试的最佳实践。您可以通过两种方式生成系统测试

  1. 使用脚手架时,明确启用系统测试
   $ bin/rails generate scaffold Article title:string body:text --system-tests=true
  1. 为关键功能独立生成系统测试
   $ bin/rails generate system_test articles

Rails 系统测试存储在您应用程序的 test/system 目录中。要生成系统测试骨架,请运行以下命令

$ bin/rails generate system_test users
      invoke test_unit
      create test/system/users_test.rb

这是一个新生成的系统测试的示例

require "application_system_test_case"

class UsersTest < ApplicationSystemTestCase
  # test "visiting the index" do
  #   visit users_url
  #
  #   assert_dom "h1", text: "Users"
  # end
end

默认情况下,系统测试使用 Selenium 驱动程序、Chrome 浏览器和 1400x1400 的屏幕尺寸运行。下一节将解释如何更改默认设置。

7.3. 更改默认设置

Rails 使更改系统测试的默认设置变得非常简单。所有设置都经过抽象,因此您可以专注于编写测试。

当您生成新应用程序或脚手架时,会在测试目录中创建一个 application_system_test_case.rb 文件。这是您系统测试的所有配置都应该存在的地方。

如果您想更改默认设置,可以更改系统测试的“驱动方式”。如果您想将驱动程序从 Selenium 更改为 Cuprite,您需要将 cuprite gem 添加到您的 Gemfile。然后在您的 application_system_test_case.rb 文件中执行以下操作

require "test_helper"
require "capybara/cuprite"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :cuprite
end

驱动名称是 driven_by 的必需参数。可以传递给 driven_by 的可选参数是 :using 用于浏览器(仅由 Selenium 使用),:screen_size 用于更改屏幕截图的屏幕尺寸,以及 :options 用于设置驱动程序支持的选项。

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :firefox
end

如果您想使用无头浏览器,可以使用无头 Chrome 或无头 Firefox,方法是在 :using 参数中添加 headless_chromeheadless_firefox

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome
end

如果要使用远程浏览器,例如 Docker 中的无头 Chrome,您必须添加一个远程 url 并通过 optionsbrowser 设置为远程。

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  url = ENV.fetch("SELENIUM_REMOTE_URL", nil)
  options = if url
    { browser: :remote, url: url }
  else
    { browser: :chrome }
  end
  driven_by :selenium, using: :headless_chrome, options: options
end

现在您应该可以连接到远程浏览器了。

$ SELENIUM_REMOTE_URL=https://:4444/wd/hub bin/rails test:system

如果您的应用程序是远程的,例如在 Docker 容器中,Capybara 需要更多关于如何调用远程服务器的输入。

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  setup do
      Capybara.server_host = "0.0.0.0" # bind to all interfaces
      Capybara.app_host = "http://#{IPSocket.getaddress(Socket.gethostname)}" if ENV["SELENIUM_REMOTE_URL"].present?
    end
  # ...
end

现在,无论是在 Docker 容器还是 CI 中运行,您都应该可以连接到远程浏览器和服务器。

如果您的 Capybara 配置需要比 Rails 提供的更多设置,则可以将此附加配置添加到 application_system_test_case.rb 文件中。

请参阅Capybara 文档以获取更多设置。

7.4. 实现系统测试

本节将演示如何向您的应用程序添加系统测试,该测试将测试访问索引页面以创建新的博客文章。

脚手架生成器默认不再创建系统测试。要在使用脚手架时包含系统测试,请使用 --system-tests=true 选项。否则,请为您的关键用户路径手动创建系统测试。

$ bin/rails generate system_test articles

它应该已经创建了一个测试文件占位符。在前面的命令的输出中,您应该会看到

      invoke  test_unit
      create    test/system/articles_test.rb

现在,让我们打开该文件并编写第一个断言

require "application_system_test_case"

class ArticlesTest < ApplicationSystemTestCase
  test "viewing the index" do
    visit articles_path
    assert_selector "h1", text: "Articles"
  end
end

测试应该会看到文章索引页面上有一个 h1 并通过。

运行系统测试。

$ bin/rails test:system

默认情况下,运行 bin/rails test 不会运行您的系统测试。请确保运行 bin/rails test:system 以实际运行它们。您还可以运行 bin/rails test:all 来运行所有测试,包括系统测试。

7.4.1. 创建文章系统测试

现在您可以测试创建新文章的流程。

test "should create Article" do
  visit articles_path

  click_on "New Article"

  fill_in "Title", with: "Creating an Article"
  fill_in "Body", with: "Created this article successfully!"

  click_on "Create Article"

  assert_text "Creating an Article"
end

第一步是调用 visit articles_path。这将把测试带到文章索引页面。

然后 click_on "New Article" 将在索引页面上找到“New Article”按钮。这将把浏览器重定向到 /articles/new

然后测试将用指定的文本填充文章的标题和正文。填写字段后,点击“Create Article”将向 /articles/create 发送 POST 请求。

这将用户重定向回文章索引页面,并在那里断言新文章标题的文本在文章索引页面上。

7.4.2. 测试多种屏幕尺寸

如果您除了测试桌面尺寸外,还想测试移动设备尺寸,您可以创建另一个继承自 ActionDispatch::SystemTestCase 的类,并在您的测试套件中使用它。在此示例中,在 /test 目录中创建了一个名为 mobile_system_test_case.rb 的文件,其配置如下。

require "test_helper"

class MobileSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :chrome, screen_size: [375, 667]
end

要使用此配置,请在 test/system 中创建一个继承自 MobileSystemTestCase 的测试。现在您可以使用多种不同的配置来测试您的应用程序。

require "mobile_system_test_case"

class PostsTest < MobileSystemTestCase
  test "visiting the index" do
    visit posts_url
    assert_selector "h1", text: "Posts"
  end
end

7.4.3. Capybara 断言

以下是 Capybara 提供的一些可在系统测试中使用的断言摘要。

断言 用途
assert_button(locator = nil, **options, &optional_filter_block) 检查页面是否具有给定文本、值或 ID 的按钮。
assert_current_path(string, **options) 断言页面具有给定路径。
assert_field(locator = nil, **options, &optional_filter_block) 检查页面是否具有给定标签、名称或 ID 的表单字段。
assert_link(locator = nil, **options, &optional_filter_block) 检查页面是否具有给定文本或 ID 的链接。
assert_selector(*args, &optional_filter_block) 断言给定选择器在页面上。
assert_table(locator = nil, **options, &optional_filter_block 检查页面是否具有给定 ID 或标题的表格。
assert_text(type, text, **options) 断言页面具有给定文本内容。

7.4.4. 屏幕截图助手

ScreenshotHelper 是一个旨在捕获测试屏幕截图的助手。这对于在测试失败时查看浏览器,或稍后查看屏幕截图进行调试很有帮助。

提供了两种方法:take_screenshottake_failed_screenshottake_failed_screenshot 会自动包含在 Rails 内部的 before_teardown 中。

take_screenshot 辅助方法可以包含在您测试中的任何位置,以捕获浏览器的屏幕截图。

7.4.5. 进一步探索

系统测试类似于集成测试,因为它测试用户与控制器、模型和视图的交互,但系统测试将您的应用程序视为一个真实用户正在使用它。通过系统测试,您可以测试用户在应用程序中会做的任何事情,例如评论、删除文章、发布草稿文章等。

8. 测试帮助方法

为了避免代码重复,您可以添加自己的测试帮助方法。这是一个登录的示例

# test/test_helper.rb

module SignInHelper
  def sign_in_as(user)
    post sign_in_url(email: user.email, password: user.password)
  end
end

class ActionDispatch::IntegrationTest
  include SignInHelper
end
require "test_helper"

class ProfileControllerTest < ActionDispatch::IntegrationTest
  test "should show profile" do
    # helper is now reusable from any controller test case
    sign_in_as users(:david)

    get profile_url
    assert_response :success
  end
end

8.1. 使用单独的文件

如果您发现您的帮助方法使 test_helper.rb 变得混乱,您可以将它们提取到单独的文件中。一个好的存储位置是 test/libtest/test_helpers

# test/test_helpers/multiple_assertions.rb
module MultipleAssertions
  def assert_multiple_of_forty_two(number)
    assert (number % 42 == 0), "expected #{number} to be a multiple of 42"
  end
end

然后可以根据需要显式地引入和包含这些帮助方法

require "test_helper"
require "test_helpers/multiple_assertions"

class NumberTest < ActiveSupport::TestCase
  include MultipleAssertions

  test "420 is a multiple of 42" do
    assert_multiple_of_forty_two 420
  end
end

它们也可以继续直接包含在相关的父类中

# test/test_helper.rb
require "test_helpers/sign_in_helper"

class ActionDispatch::IntegrationTest
  include SignInHelper
end

8.2. 急切地引入帮助方法

您可能会发现急切地在 test_helper.rb 中引入帮助方法很方便,这样您的测试文件就可以隐式访问它们。这可以通过使用 globbing 来实现,如下所示

# test/test_helper.rb
Dir[Rails.root.join("test", "test_helpers", "**", "*.rb")].each { |file| require file }

这样做的一个缺点是会增加启动时间,而不是在您的单个测试中手动引入所需的唯一文件。

9. 测试路由

和您的 Rails 应用程序中的所有其他东西一样,您可以测试您的路由。路由测试存储在 test/controllers/ 中,或者作为控制器测试的一部分。如果您的应用程序有复杂的路由,Rails 提供了许多有用的帮助方法来测试它们。

有关 Rails 中可用的路由断言的更多信息,请参阅 ActionDispatch::Assertions::RoutingAssertions 的 API 文档。

10. 测试视图

通过断言关键 HTML 元素及其内容的存在来测试对您请求的响应是测试应用程序视图的一种方式。与路由测试一样,视图测试存储在 test/controllers/ 中或作为控制器测试的一部分。

10.1. 查询 HTML

assert_domassert_dom_equal 等方法允许您使用简单而强大的语法查询响应的 HTML 元素。

assert_dom 是一个断言,如果找到匹配的元素,它将返回 true。例如,您可以如下验证页面标题是否为“Welcome to the Rails Testing Guide”

assert_dom "title", "Welcome to the Rails Testing Guide"

您还可以使用嵌套的 assert_dom 块进行更深入的调查。

在以下示例中,内部 assert_dom 用于 li.menu_item 在由外部块选择的元素集合中运行

assert_dom "ul.navigation" do
  assert_dom "li.menu_item"
end

选定元素的集合也可以遍历,以便可以为每个元素单独调用 assert_dom。例如,如果响应包含两个有序列表,每个列表有四个嵌套列表元素,则以下两个测试都将通过。

assert_dom "ol" do |elements|
  elements.each do |element|
    assert_dom element, "li", 4
  end
end

assert_dom "ol" do
  assert_dom "li", 8
end

assert_dom_equal 方法比较两个 HTML 字符串以查看它们是否相等

assert_dom_equal '<a href="http://www.further-reading.com">Read more</a>',
  link_to("Read more", "http://www.further-reading.com")

有关高级用法,请参阅 rails-dom-testing 文档

为了与 rails-dom-testing 集成,继承自 ActionView::TestCase 的测试声明了一个 document_root_element 方法,该方法将渲染的内容作为 Nokogiri::XML::Node 的实例返回

test "renders a link to itself" do
  article = Article.create! title: "Hello, world"

  render "articles/article", article: article
  anchor = document_root_element.at("a")

  assert_equal article.name, anchor.text
  assert_equal article_url(article), anchor["href"]
end

如果您的应用程序依赖于 Nokogiri >= 1.14.0 或更高版本,以及 minitest >= 5.18.0,则 document_root_element 支持 Ruby 的模式匹配

test "renders a link to itself" do
  article = Article.create! title: "Hello, world"

  render "articles/article", article: article
  anchor = document_root_element.at("a")
  url = article_url(article)

  assert_pattern do
    anchor => { content: "Hello, world", attributes: [{ name: "href", value: url }] }
  end
end

如果您想访问 系统测试 所使用的相同的 Capybara 驱动的断言,您可以定义一个继承自 ActionView::TestCase 的基类,并将 document_root_element 转换为 page 方法

# test/view_partial_test_case.rb

require "test_helper"
require "capybara/minitest"

class ViewPartialTestCase < ActionView::TestCase
  include Capybara::Minitest::Assertions

  def page
    Capybara.string(rendered)
  end
end

# test/views/article_partial_test.rb

require "view_partial_test_case"

class ArticlePartialTest < ViewPartialTestCase
  test "renders a link to itself" do
    article = Article.create! title: "Hello, world"

    render "articles/article", article: article

    assert_link article.title, href: article_url(article)
  end
end

有关 Capybara 包含的断言的更多信息,请参见Capybara 断言部分。

10.2. 解析视图内容

从 Action View 7.1 版本开始,rendered 帮助方法返回一个能够解析视图局部变量渲染内容的 对象。

要将 rendered 方法返回的 String 内容转换为对象,请通过调用 register_parser 定义一个解析器。调用 register_parser :rss 定义了一个 rendered.rss 帮助方法。例如,要将渲染的 RSS 内容 解析为带有 rendered.rss 的对象,请注册对 RSS::Parser.parse 的调用

register_parser :rss, -> rendered { RSS::Parser.parse(rendered) }

test "renders RSS" do
  article = Article.create!(title: "Hello, world")

  render formats: :rss, partial: article

  assert_equal "Hello, world", rendered.rss.items.last.title
end

默认情况下,ActionView::TestCase 为以下内容定义了解析器

test "renders HTML" do
  article = Article.create!(title: "Hello, world")

  render partial: "articles/article", locals: { article: article }

  assert_pattern { rendered.html.at("main h1") => { content: "Hello, world" } }
end

test "renders JSON" do
  article = Article.create!(title: "Hello, world")

  render formats: :json, partial: "articles/article", locals: { article: article }

  assert_pattern { rendered.json => { title: "Hello, world" } }
end

10.3. 额外的基于视图的断言

还有更多主要用于测试视图的断言

断言 用途
assert_dom_email 允许您对电子邮件正文进行断言。
assert_dom_encoded 允许您对编码的 HTML 进行断言。它通过解码每个元素的内容,然后使用所有未编码的元素调用块来完成此操作。
css_select(selector)css_select(element, selector) 返回由 selector 选择的所有元素的数组。在第二种变体中,它首先匹配基本 element,然后尝试在其任何子元素上匹配 selector 表达式。如果没有匹配项,两种变体都返回空数组。

这是使用 assert_dom_email 的一个示例

assert_dom_email do
  assert_dom "small", "Please click the 'Unsubscribe' link if you want to opt-out."
end

10.4. 测试视图局部变量

局部变量模板——通常称为“partials”——可以将渲染过程分解为更易于管理的块。通过局部变量,您可以将视图中的代码片段提取到单独的文件中,并在多个位置重用它们。

视图测试提供了测试局部变量按预期渲染内容的机会。视图局部变量测试可以存储在 test/views/ 中并继承自 ActionView::TestCase

要渲染局部变量,请像在模板中一样调用 render。内容可以通过测试本地的 rendered 方法获取

class ArticlePartialTest < ActionView::TestCase
  test "renders a link to itself" do
    article = Article.create! title: "Hello, world"

    render "articles/article", article: article

    assert_includes rendered, article.title
  end
end

继承自 ActionView::TestCase 的测试还可以访问 assert_domrails-dom-testing 提供的其他基于视图的断言

test "renders a link to itself" do
  article = Article.create! title: "Hello, world"

  render "articles/article", article: article

  assert_dom "a[href=?]", article_url(article), text: article.title
end

10.5. 测试视图帮助方法

帮助方法是一个模块,您可以在其中定义在视图中可用的方法。

为了测试帮助方法,您所要做的就是检查帮助方法方法的输出是否与您预期的匹配。与帮助方法相关的测试位于 test/helpers 目录下。

假设我们有以下帮助方法

module UsersHelper
  def link_to_user(user)
    link_to "#{user.first_name} #{user.last_name}", user
  end
end

我们可以像这样测试此方法的输出

class UsersHelperTest < ActionView::TestCase
  test "should return the user's full name" do
    user = users(:david)

    assert_dom_equal %{<a href="/user/#{user.id}">David Heinemeier Hansson</a>}, link_to_user(user)
  end
end

此外,由于测试类继承自 ActionView::TestCase,因此您可以访问 Rails 的帮助方法,例如 link_topluralize

11. 测试邮件程序

您的邮件程序类——就像您的 Rails 应用程序的其他部分一样——应该进行测试,以确保它们按预期工作。

测试您的邮件程序类的目标是确保

  • 电子邮件正在处理(创建和发送)
  • 电子邮件内容正确(主题、发件人、正文等)
  • 正确的电子邮件在正确的时间发送

测试您的邮件程序有两个方面,即单元测试和功能测试。在单元测试中,您独立运行邮件程序,使用严格控制的输入,并将输出与已知值(固定数据)进行比较。在功能测试中,您不必过多测试邮件程序生成的细节;相反,您测试控制器和模型是否以正确的方式使用邮件程序。您测试以证明正确的电子邮件在正确的时间发送了。

11.1. 单元测试

为了测试您的邮件程序是否按预期工作,您可以使用单元测试将邮件程序的实际结果与预先编写的应该生成的结果示例进行比较。

11.1.1. 邮件程序固定数据

为了对邮件程序进行单元测试,固定数据用于提供输出“应该”如何的示例。由于这些是示例电子邮件,而不是像其他固定数据那样的 Active Record 数据,因此它们与P其他固定数据分开保存在自己的子目录中。test/fixtures 内的目录名称直接对应于邮件程序的名称。因此,对于名为 UserMailer 的邮件程序,固定数据应位于 test/fixtures/user_mailer 目录中。

如果您生成了邮件程序,生成器不会为邮件程序操作创建存根固定数据。您必须如上所述自行创建这些文件。

11.1.2. 基本测试用例

这是一个测试名为 UserMailer 的邮件程序的单元测试,其动作 invite 用于向朋友发送邀请

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # Create the email and store it for further assertions
    email = UserMailer.create_invite("me@example.com",
                                     "friend@example.com", Time.now)

    # Send the email, then test that it got queued
    assert_emails 1 do
      email.deliver_now
    end

    # Test the body of the sent email contains what we expect it to
    assert_equal ["me@example.com"], email.from
    assert_equal ["friend@example.com"], email.to
    assert_equal "You have been invited by me@example.com", email.subject
    assert_equal read_fixture("invite").join, email.body.to_s
  end
end

在测试中,电子邮件被创建,并且返回的对象存储在 email 变量中。第一个断言检查它是否已发送,然后在第二批断言中检查电子邮件内容。帮助方法 read_fixture 用于读取此文件中的内容。

当只有一个(HTML 或文本)部分存在时,email.body.to_s 存在。如果邮件程序同时提供两者,您可以将您的固定数据与 email.text_part.body.to_semail.html_part.body.to_s 的特定部分进行测试。

这是 invite 固定数据的内容

Hi friend@example.com,

You have been invited.

Cheers!

11.1.3. 配置测试的发送方法

config/environments/test.rb 中的 ActionMailer::Base.delivery_method = :test 行将发送方法设置为测试模式,这样电子邮件实际上不会发送(这在测试时避免向用户发送垃圾邮件很有用)。相反,电子邮件将附加到一个数组 (ActionMailer::Base.deliveries) 中。

ActionMailer::Base.deliveries 数组仅在 ActionMailer::TestCaseActionDispatch::IntegrationTest 测试中自动重置。如果您想在这些测试用例之外拥有一个干净的开始,您可以手动重置它:ActionMailer::Base.deliveries.clear

11.1.4. 测试排队电子邮件

您可以使用 assert_enqueued_email_with 断言来确认电子邮件已与所有预期的邮件程序方法参数和/或参数化邮件程序参数一起排队。这允许您匹配任何已使用 deliver_later 方法排队的电子邮件。

与基本测试用例一样,我们创建电子邮件并将返回的对象存储在 email 变量中。以下示例包括传递参数和/或参数的变体。

此示例将断言电子邮件已与正确的参数一起排队

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # Create the email and store it for further assertions
    email = UserMailer.create_invite("me@example.com", "friend@example.com")

    # Test that the email got enqueued with the correct arguments
    assert_enqueued_email_with UserMailer, :create_invite, args: ["me@example.com", "friend@example.com"] do
      email.deliver_later
    end
  end
end

此示例将断言邮件程序已与正确的命名参数一起排队,通过将参数的哈希作为 args 传递

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # Create the email and store it for further assertions
    email = UserMailer.create_invite(from: "me@example.com", to: "friend@example.com")

    # Test that the email got enqueued with the correct named arguments
    assert_enqueued_email_with UserMailer, :create_invite,
    args: [{ from: "me@example.com", to: "friend@example.com" }] do
      email.deliver_later
    end
  end
end

此示例将断言参数化邮件程序已与正确的参数和参数一起排队。邮件程序参数作为 params 传递,邮件程序方法参数作为 args 传递

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # Create the email and store it for further assertions
    email = UserMailer.with(all: "good").create_invite("me@example.com", "friend@example.com")

    # Test that the email got enqueued with the correct mailer parameters and arguments
    assert_enqueued_email_with UserMailer, :create_invite,
    params: { all: "good" }, args: ["me@example.com", "friend@example.com"] do
      email.deliver_later
    end
  end
end

此示例显示了测试参数化邮件程序是否已使用正确参数排队的另一种方式

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # Create the email and store it for further assertions
    email = UserMailer.with(to: "friend@example.com").create_invite

    # Test that the email got enqueued with the correct mailer parameters
    assert_enqueued_email_with UserMailer.with(to: "friend@example.com"), :create_invite do
      email.deliver_later
    end
  end
end

11.2. 功能和系统测试

单元测试允许我们测试电子邮件的属性,而功能和系统测试允许我们测试用户交互是否适当地触发电子邮件发送。例如,您可以检查邀请朋友操作是否适当地发送电子邮件

# Integration Test
require "test_helper"

class UsersControllerTest < ActionDispatch::IntegrationTest
  test "invite friend" do
    # Asserts the difference in the ActionMailer::Base.deliveries
    assert_emails 1 do
      post invite_friend_url, params: { email: "friend@example.com" }
    end
  end
end
# System Test
require "test_helper"

class UsersTest < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome

  test "inviting a friend" do
    visit invite_users_url
    fill_in "Email", with: "friend@example.com"
    assert_emails 1 do
      click_on "Invite"
    end
  end
end

assert_emails 方法不与特定的发送方法绑定,并且适用于使用 deliver_nowdeliver_later 方法发送的电子邮件。如果我们明确想要断言电子邮件已排队,我们可以使用 assert_enqueued_email_with以上示例)或 assert_enqueued_emails 方法。更多信息可以在文档中找到。

12. 测试作业

作业可以独立测试(侧重于作业的行为)和在上下文中测试(侧重于调用代码的行为)。

12.1. 独立测试作业

当你生成一个作业时,一个相关的测试文件也会在 test/jobs 目录中生成。

这是一个你可以为计费作业编写的测试:

require "test_helper"

class BillingJobTest < ActiveJob::TestCase
  test "account is charged" do
    perform_enqueued_jobs do
      BillingJob.perform_later(account, product)
    end
    assert account.reload.charged_for?(product)
  end
end

默认的测试队列适配器在调用 perform_enqueued_jobs 之前不会执行作业。此外,它会在每个测试运行之前清除所有作业,这样测试就不会相互干扰。

该测试使用 perform_enqueued_jobsperform_later 而不是 perform_now,这样如果配置了重试,重试失败就会被测试捕获,而不是被重新入队并忽略。

12.2. 在上下文中测试作业

测试作业是否正确入队(例如,通过控制器动作)是一个好的做法。ActiveJob::TestHelper 模块提供了几种可以帮助实现此目的的方法,例如 assert_enqueued_with

这是一个测试账户模型方法的示例:

require "test_helper"

class AccountTest < ActiveSupport::TestCase
  include ActiveJob::TestHelper

  test "#charge_for enqueues billing job" do
    assert_enqueued_with(job: BillingJob) do
      account.charge_for(product)
    end

    assert_not account.reload.charged_for?(product)

    perform_enqueued_jobs

    assert account.reload.charged_for?(product)
  end
end

12.3. 测试异常是否被抛出

测试作业在某些情况下是否抛出异常可能很棘手,特别是当你配置了重试时。perform_enqueued_jobs 助手会使任何作业抛出异常的测试失败,因此要在抛出异常时使测试成功,你必须直接调用作业的 perform 方法。

require "test_helper"

class BillingJobTest < ActiveJob::TestCase
  test "does not charge accounts with insufficient funds" do
    assert_raises(InsufficientFundsError) do
      BillingJob.new(empty_account, product).perform
    end
    assert_not account.reload.charged_for?(product)
  end
end

通常不建议使用此方法,因为它会规避框架的某些部分,例如参数序列化。

13. 测试 Action Cable

由于 Action Cable 在你的应用程序中用于不同的级别,你需要同时测试通道、连接类本身,以及其他实体是否广播正确的消息。

13.1. 连接测试用例

默认情况下,当你使用 Action Cable 生成一个新的 Rails 应用程序时,一个用于基本连接类 (ApplicationCable::Connection) 的测试也会在 test/channels/application_cable 目录下生成。

连接测试旨在检查连接的标识符是否正确分配,或者任何不当的连接请求是否被拒绝。这是一个示例:

class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
  test "connects with params" do
    # Simulate a connection opening by calling the `connect` method
    connect params: { user_id: 42 }

    # You can access the Connection object via `connection` in tests
    assert_equal connection.user_id, "42"
  end

  test "rejects connection without params" do
    # Use `assert_reject_connection` matcher to verify that
    # connection is rejected
    assert_reject_connection { connect }
  end
end

你也可以像在集成测试中一样指定请求 cookie:

test "connects with cookies" do
  cookies.signed[:user_id] = "42"

  connect

  assert_equal connection.user_id, "42"
end

有关更多信息,请参阅 ActionCable::Connection::TestCase 的 API 文档。

13.2. 通道测试用例

默认情况下,当你生成一个通道时,一个相关的测试也会在 test/channels 目录下生成。这是一个带有聊天通道的示例测试:

require "test_helper"

class ChatChannelTest < ActionCable::Channel::TestCase
  test "subscribes and stream for room" do
    # Simulate a subscription creation by calling `subscribe`
    subscribe room: "15"

    # You can access the Channel object via `subscription` in tests
    assert subscription.confirmed?
    assert_has_stream "chat_15"
  end
end

这个测试非常简单,只断言通道将连接订阅到特定的流。

你还可以指定底层的连接标识符。这是一个带有网络通知通道的示例测试:

require "test_helper"

class WebNotificationsChannelTest < ActionCable::Channel::TestCase
  test "subscribes and stream for user" do
    stub_connection current_user: users(:john)

    subscribe

    assert_has_stream_for users(:john)
  end
end

有关更多信息,请参阅 ActionCable::Channel::TestCase 的 API 文档。

13.3. 自定义断言和在其他组件中测试广播

Action Cable 附带了一系列自定义断言,可用于减少测试的冗余。有关可用断言的完整列表,请参阅 ActionCable::TestHelper 的 API 文档。

确保在其他组件(例如在控制器内部)中广播了正确的消息是一个好的做法。这正是 Action Cable 提供的自定义断言非常有用之处。例如,在模型内部:

require "test_helper"

class ProductTest < ActionCable::TestCase
  test "broadcast status after charge" do
    assert_broadcast_on("products:#{product.id}", type: "charged") do
      product.charge(account)
    end
  end
end

如果你想测试使用 Channel.broadcast_to 进行的广播,你应该使用 Channel.broadcasting_for 来生成底层流名称:

# app/jobs/chat_relay_job.rb
class ChatRelayJob < ApplicationJob
  def perform(room, message)
    ChatChannel.broadcast_to room, text: message
  end
end
# test/jobs/chat_relay_job_test.rb
require "test_helper"

class ChatRelayJobTest < ActiveJob::TestCase
  include ActionCable::TestHelper

  test "broadcast message to room" do
    room = rooms(:all)

    assert_broadcast_on(ChatChannel.broadcasting_for(room), text: "Hi!") do
      ChatRelayJob.perform_now(room, "Hi!")
    end
  end
end

14. 在持续集成 (CI) 中运行测试

持续集成 (CI) 是一种开发实践,其中更改频繁集成到主代码库中,因此在合并之前会自动进行测试。

要在 CI 环境中运行所有测试,你只需要一个命令:

$ bin/rails test

如果你正在使用系统测试bin/rails test 将不会运行它们,因为它们可能很慢。要也运行它们,请添加另一个 CI 步骤来运行 bin/rails test:system,或者将你的第一个步骤更改为 bin/rails test:all,它会运行所有测试,包括系统测试。

15. 并行测试

并行运行测试可以减少整个测试套件运行所需的时间。虽然分叉进程是默认方法,但也支持线程。

15.1. 使用进程进行并行测试

默认的并行化方法是使用 Ruby 的 DRb 系统分叉进程。进程根据提供的worker数量分叉。默认数量是机器上的实际核心数量,但可以通过传递给 parallelize 方法的数字进行更改。

要启用并行化,请将以下内容添加到你的 test_helper.rb 中:

class ActiveSupport::TestCase
  parallelize(workers: 2)
end

传递的 worker 数量是进程将被分叉的次数。你可能希望你的本地测试套件与你的 CI 不同地并行化,因此提供了一个环境变量,以便能够轻松更改测试运行应使用的 worker 数量:

$ PARALLEL_WORKERS=15 bin/rails test

在并行化测试时,Active Record 会自动处理为每个进程创建数据库并将模式加载到数据库中。数据库将以与 worker 对应的数字为后缀。例如,如果你有 2 个 worker,测试将分别创建 test-database-0test-database-1

如果传递的 worker 数量是 1 或更少,则进程将不会被分叉,测试也不会并行化,它们将使用原始的 test-database 数据库。

提供了两个钩子,一个在进程分叉时运行,一个在分叉进程关闭之前运行。如果你的应用程序使用多个数据库或执行其他依赖于 worker 数量的任务,这些钩子会很有用。

parallelize_setup 方法在进程分叉后立即调用。parallelize_teardown 方法在进程关闭之前立即调用。

class ActiveSupport::TestCase
  parallelize_setup do |worker|
    # setup databases
  end

  parallelize_teardown do |worker|
    # cleanup databases
  end

  parallelize(workers: :number_of_processors)
end

使用线程进行并行测试时,不需要或不可用这些方法。

15.2. 使用线程进行并行测试

如果你喜欢使用线程或正在使用 JRuby,则提供了线程并行化选项。线程并行化器由 minitest 的 Parallel::Executor 支持。

要将并行化方法更改为使用线程而不是分叉,请将以下内容放入你的 test_helper.rb 中:

class ActiveSupport::TestCase
  parallelize(workers: :number_of_processors, with: :threads)
end

由 JRuby 或 TruffleRuby 生成的 Rails 应用程序将自动包含 with: :threads 选项。

如上节所述,你也可以在此上下文中使用环境变量 PARALLEL_WORKERS 来更改你的测试运行应使用的 worker 数量。

15.3. 测试并行事务

当你想测试在线程中运行并行数据库事务的代码时,这些事务可能会相互阻塞,因为它们已经嵌套在隐式测试事务之下。

为了解决这个问题,你可以通过设置 self.use_transactional_tests = false 来禁用测试用例类中的事务。

class WorkerTest < ActiveSupport::TestCase
  self.use_transactional_tests = false

  test "parallel transactions" do
    # start some threads that create transactions
  end
end

在禁用事务测试的情况下,你必须清理测试创建的任何数据,因为在测试完成后更改不会自动回滚。

15.4. 并行测试的阈值

并行运行测试会增加数据库设置和夹具加载的开销。因此,Rails 不会对涉及少于 50 个测试的执行进行并行化。

你可以在 test.rb 中配置此阈值:

config.active_support.test_parallelization_threshold = 100

也可以在测试用例级别设置并行化时进行配置:

class ActiveSupport::TestCase
  parallelize threshold: 100
end

16. 测试预加载

通常,应用程序在 developmenttest 环境中不会预加载以加快速度。但在 production 环境中会预加载。

如果项目中的某些文件由于某种原因无法加载,在部署到生产环境之前检测到它很重要。

16.1. 持续集成

如果你的项目有 CI,在 CI 中预加载是确保应用程序预加载的简单方法。

CI 通常会设置一个环境变量来指示测试套件正在那里运行。例如,它可以是 CI

# config/environments/test.rb
config.eager_load = ENV["CI"].present?

从 Rails 7 开始,新生成的应用程序默认以这种方式配置。

如果你的项目没有持续集成,你仍然可以通过调用 Rails.application.eager_load! 在测试套件中进行预加载:

require "test_helper"

class ZeitwerkComplianceTest < ActiveSupport::TestCase
  test "eager loads all files without errors" do
    assert_nothing_raised { Rails.application.eager_load! }
  end
end

17. 其他测试资源

17.1. 错误

在系统测试、集成测试和功能控制器测试中,Rails 默认会尝试从引发的错误中恢复并使用 HTML 错误页面进行响应。此行为可以通过 config.action_dispatch.show_exceptions 配置进行控制。

17.2. 测试时间依赖代码

Rails 提供了内置的辅助方法,使你能够断言你的时间敏感代码按预期工作。

以下示例使用 travel_to 辅助方法:

# Given a user is eligible for gifting a month after they register.
user = User.create(name: "Gaurish", activation_date: Date.new(2004, 10, 24))
assert_not user.applicable_for_gifting?

travel_to Date.new(2004, 11, 24) do
  # Inside the `travel_to` block `Date.current` is stubbed
  assert_equal Date.new(2004, 10, 24), user.activation_date
  assert user.applicable_for_gifting?
end

# The change was visible only inside the `travel_to` block.
assert_equal Date.new(2004, 10, 24), user.activation_date

有关可用时间辅助方法的更多信息,请参阅 ActiveSupport::Testing::TimeHelpers API 参考。



回到顶部