v7.1.3.2
rubyonrails.org: 了解更多資訊: 更多 Ruby on Rails

測試 Rails 應用程式

本指南涵蓋 Rails 中用於測試應用程式的內建機制。

閱讀本指南後,您將會了解

1 為什麼要為 Rails 應用程式撰寫測試?

Rails 讓撰寫測試變得非常容易。它會在您建立模型和控制器時產生測試程式碼的雛形。

透過執行 Rails 測試,您可以在進行一些重大的程式碼重構後,確保程式碼符合預期的功能。

Rails 測試也可以模擬瀏覽器要求,因此您可以在不透過瀏覽器測試的情況下,測試應用程式的回應。

2 測試簡介

測試支援從一開始就編織到 Rails 架構中。它並不是「喔!讓我們加入執行測試的支援,因為它們很新很酷」的頓悟。

2.1 Rails 從一開始就設定好測試

只要使用 rails new 應用程式名稱 建立 Rails 專案,Rails 就會為您建立一個 test 目錄。如果您列出此目錄的內容,您會看到

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

helpersmailersmodels 目錄分別用於存放檢視輔助程式、郵件和模型的測試。channels 目錄用於存放 Action Cable 連線和頻道的測試。controllers 目錄用於存放控制器、路由和檢視的測試。integration 目錄用於存放控制器之間互動的測試。

系統測試目錄存放系統測試,用於對應用程式進行完整的瀏覽器測試。系統測試讓您可以像使用者一樣測試應用程式,並協助您測試 JavaScript。系統測試繼承自 Capybara,並對應用程式執行瀏覽器測試。

固定裝置是一種組織測試資料的方式;它們位於 fixtures 目錄中。

當首次產生關聯測試時,也會建立一個 jobs 目錄。

test_helper.rb 檔案存放測試的預設組態。

application_system_test_case.rb 存放系統測試的預設組態。

2.2 測試環境

預設情況下,每個 Rails 應用程式有三個環境:開發、測試和生產。

每個環境的組態都可以用類似的方式修改。在本例中,我們可以透過變更 config/environments/test.rb 中的選項來修改我們的測試環境。

您的測試會在 RAILS_ENV=test 下執行。

2.3 Rails 符合 Minitest

如果您還記得,我們在 Rails 入門 指南中使用了 bin/rails generate model 指令。我們建立了第一個模型,其中包含在 test 目錄中建立測試 stub。

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

test/models/article_test.rb 中的預設測試 stub 如下所示

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。因此,ArticleTest 具有 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.3.1 您的第一次失敗測試

若要查看測試失敗的報告方式,您可以將失敗的測試新增至 article_test.rb 測試案例。

test "should not save article without title" do
  article = Article.new
  assert_not article.save
end

讓我們執行這個新增加的測試(其中 6 是定義測試的行號)。

$ bin/rails test test/models/article_test.rb:6
Run options: --seed 44656

# Running:

F

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


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



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

現在測試應該會通過。讓我們再次執行測試來驗證

$ bin/rails test test/models/article_test.rb:6
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.3.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
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」。它表示有錯誤的測試。

只要遇到任何錯誤或斷言失敗,每個測試方法的執行就會停止,而測試套件會繼續執行下一個方法。所有測試方法都會以隨機順序執行。可以使用 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.4 可用的斷言

現在你已經看到一些可用的斷言。斷言是測試的工蜂。它們實際上執行檢查以確保事情按計劃進行。

以下是你可以與 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] ) 確保數字 expectedactual 在彼此 delta 範圍內。
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 的實例或繼承自 class
assert_not_kind_of( class, obj, [msg] ) 確保 obj 不是 class 的實例或未繼承自 class
assert_respond_to( obj, symbol, [msg] ) 確保 objsymbol 有回應。
assert_not_respond_to( obj, symbol, [msg] ) 確保 objsymbol 沒有回應。
assert_operator( obj1, operator, [obj2], [msg] ) 確保 obj1.operator(obj2) 為真。
assert_not_operator( obj1, operator, [obj2], [msg] ) 確保 obj1.operator(obj2) 為 false。
assert_predicate ( obj, predicate, [msg] ) 確保 obj.predicate 為 true,例如 assert_predicate str, :empty?
assert_not_predicate ( obj, predicate, [msg] ) 確保 obj.predicate 為 false,例如 assert_not_predicate str, :empty?
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 }
flunk( [msg] ) 確保失敗。這有助於明確標示尚未完成的測試。

以上是 minitest 支援的斷言子集。如需完整且最新的清單,請查看 Minitest API 文件,特別是 Minitest::Assertions

由於測試架構的模組化特性,因此可以建立自己的斷言。事實上,這就是 Rails 所做的。它包含一些專門的斷言,讓您的工作更輕鬆。

建立自己的斷言是進階主題,本教學課程不會涵蓋。

2.5 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 的反向。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

您將在下一章中看到這些斷言中的一些用法。

2.6 關於測試案例的簡要說明

Minitest::Assertions 中定義的所有基本斷言,例如 assert_equal,也在我們自己的測試案例中使用的類別中提供。事實上,Rails 提供了以下類別供您繼承

這些類別每個都包含 Minitest::Assertions,讓我們可以在測試中使用所有基本斷言。

有關 Minitest 的更多資訊,請參閱 其文件

2.7 Rails 測試執行器

我們可以使用 bin/rails test 指令一次執行所有測試。

或者,我們可以透過將包含測試案例的檔案名稱傳遞給 bin/rails test 指令,來執行單一測試檔案。

$ bin/rails test test/models/article_test.rb
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
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: rails test [options] [files or directories]

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.
    -n, --name PATTERN               Filter run on /regexp/ or string.
        --exclude PATTERN            Exclude /regexp/ or string from run.

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
    -p, --pride                      Pride. Show your testing pride!

2.8 在持續整合 (CI) 中執行測試

要在 CI 環境中執行所有測試,您只需要一個指令

$ bin/rails test

如果您正在使用 系統測試bin/rails test 將不會執行它們,因為它們可能會很慢。若要執行它們,請新增另一個執行 bin/rails test:system 的 CI 步驟,或將您的第一步變更為 bin/rails test:all,它會執行所有測試,包括系統測試。

3 並行測試

平行測試讓您可以將測試套件平行化。雖然預設方法是分岔處理程序,但它也支援執行緒。平行執行測試可以縮短整個測試套件的執行時間。

3.1 使用處理程序的平行測試

預設的平行化方法是使用 Ruby 的 DRb 系統分岔處理程序。處理程序會根據提供的執行緒數目分岔。預設數目是您所在機器上的實際核心數目,但可以透過傳遞給 parallelize 方法的數目來變更。

若要啟用平行化,請將下列內容新增到您的 test_helper.rb

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

傳遞的執行緒數目是處理程序將分岔的次數。您可能想要以不同於 CI 的方式平行化您的本機測試套件,因此提供了一個環境變數,以便輕鬆變更測試執行應使用的執行緒數目

$ PARALLEL_WORKERS=15 bin/rails test

在平行化測試時,Active Record 會自動處理為每個處理程序建立資料庫並將架構載入資料庫。資料庫會加上與執行緒對應的數目。例如,如果您有 2 個執行緒,測試將分別建立 test-database-0test-database-1

如果傳遞的執行緒數目為 1 或更少,處理程序將不會分岔,且測試將不會平行化,而測試將使用原始的 test-database 資料庫。

提供兩個掛鉤,一個在處理程序分岔時執行,另一個在分岔的處理程序關閉之前執行。如果您的應用程式使用多個資料庫或執行其他取決於執行緒數目的工作,這些掛鉤會很有用。

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

在使用平行測試與執行緒時,不需要或無法使用這些方法。

3.2 使用執行緒的平行測試

如果您偏好使用執行緒或正在使用 JRuby,則提供執行緒平行化選項。執行緒平行化器由 Minitest 的 Parallel::Executor 支援。

若要變更平行化方法,以使用執行緒而非分岔,請將下列內容放入您的 test_helper.rb

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

由 JRuby 或 TruffleRuby 產生的 Rails 應用程式會自動包含 with: :threads 選項。

傳遞給 parallelize 的工作執行緒數目決定測試將使用的執行緒數目。您可能希望以不同於 CI 的方式並行執行您的本機測試套件,因此提供環境變數以方便變更測試執行應使用的執行緒數目

$ PARALLEL_WORKERS=15 bin/rails test

3.3 測試平行交易

Rails 會自動將任何測試案例包在資料庫交易中,並在測試完成後將其回滾。這讓測試案例彼此獨立,且資料庫變更只會在單一測試中可見。

當您想要測試在執行緒中執行平行交易的程式碼時,交易可能會互相阻擋,因為它們已經巢狀在測試交易之下。

您可以透過設定 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

在停用交易測試的情況下,您必須清除任何測試建立的資料,因為在測試完成後不會自動將變更回滾。

3.4 並行執行測試的臨界值

並行執行測試會增加資料庫設定和固定元件載入的負擔。因此,Rails 對於涉及少於 50 個測試的執行不會進行並行處理。

您可以在 test.rb 中設定此臨界值

config.active_support.test_parallelization_threshold = 100

以及在測試案例層級設定並行處理時

class ActiveSupport::TestCase
  parallelize threshold: 100
end

4 測試資料庫

幾乎每個 Rails 應用程式都會大量與資料庫互動,因此您的測試也需要一個資料庫來互動。若要撰寫有效率的測試,您需要了解如何設定此資料庫並使用範例資料填入資料。

預設情況下,每個 Rails 應用程式有三個環境:開發、測試和生產。每個環境的資料庫會在 config/database.yml 中設定。

專用的測試資料庫讓您可以在隔離的環境中設定和互動測試資料。這樣一來,您的測試可以放心地修改測試資料,而不用擔心開發或生產資料庫中的資料。

4.1 維護測試資料庫架構

若要執行測試,測試資料庫需要具有目前的結構。測試輔助程式會檢查測試資料庫是否有任何待處理的遷移。它會嘗試將 db/schema.rbdb/structure.sql 載入測試資料庫。如果仍有待處理的遷移,將會產生錯誤。這通常表示您的架構尚未完全遷移。針對開發資料庫執行遷移(bin/rails db:migrate)將使架構保持最新狀態。

如果現有遷移有任何修改,則需要重建測試資料庫。這可以透過執行 bin/rails db:test:prepare 來完成。

4.2 固定裝置的重點

對於良好的測試,您需要考慮設定測試資料。在 Rails 中,您可以透過定義和自訂固定裝置來處理這項工作。您可以在 固定裝置 API 文件 中找到全面的文件。

4.2.1 什麼是固定裝置?

固定裝置 是範例資料的華麗詞彙。固定裝置讓您可以在測試執行前,使用預先定義的資料填入測試資料庫。固定裝置與資料庫無關,且以 YAML 編寫。每個模型有一個檔案。

固定裝置並非設計為建立測試所需的所有物件,且僅用於可套用於一般案例的預設資料時,才能獲得最佳管理。

您可以在 test/fixtures 目錄下找到固定裝置。當您執行 bin/rails generate model 來建立新模型時,Rails 會自動在此目錄中建立固定裝置程式碼片段。

4.2.2 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
about:
  name: About
# test/fixtures/articles.yml
first:
  title: Welcome to Rails!
  category: about
# 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 中找到的第一個 Article 的 category 鍵值為 about,而 fixtures/action_text/rich_texts.yml 中找到的第一個 first_content 條目的 record 鍵值為 first (Article)。這暗示 Active Record 為前者載入在 fixtures/categories.yml 中找到的 Category about,並暗示 Action Text 為後者載入在 fixtures/articles.yml 中找到的 Article first

若要讓關聯透過名稱相互參考,您可以使用固定裝置名稱,而不是在關聯固定裝置上指定 id: 屬性。Rails 會自動指派一個主鍵,以確保執行之間的一致性。如需有關此關聯行為的更多資訊,請閱讀 固定裝置 API 文件

4.2.3 檔案附件固定裝置

與其他 Active Record 支援的模型一樣,Active Storage 附件記錄繼承自 ActiveRecord::Base 實例,因此可以使用固定裝置填入資料。

考量一個 Article 模型,它有一個關聯的影像作為 thumbnail 附件,以及固定裝置資料 YAML

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

假設在 test/fixtures/files/first.pngimage/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

4.2.4 ERB'in It Up

ERB 讓您可以在範本中嵌入 Ruby 程式碼。當 Rails 載入固定裝置時,YAML 固定裝置格式會使用 ERB 進行預處理。這讓您可以使用 Ruby 來協助您產生一些範例資料。例如,下列程式碼會產生一千個使用者

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

4.2.5 固定裝置在動作中

Rails 預設會自動載入所有來自 test/fixtures 目錄的固定裝置。載入包含三個步驟

  1. 從與固定裝置對應的資料表中移除任何現有資料
  2. 將固定裝置資料載入資料表中
  3. 將固定裝置資料傾印到方法中,以防你想要直接存取它

為了從資料庫中移除現有資料,Rails 會嘗試停用參考完整性觸發器(例如外來鍵和檢查約束)。如果你在執行測試時收到惱人的權限錯誤,請確認資料庫使用者具有在測試環境中停用這些觸發器的權限。(在 PostgreSQL 中,只有超級使用者可以停用所有觸發器。在此處閱讀更多關於 PostgreSQL 權限的資訊 here)。

4.2.6 固定裝置是 Active Record 物件

固定裝置是 Active Record 的執行個體。如上文第 3 點所述,你可以直接存取物件,因為它會自動作為測試案例的局部範圍方法提供。例如

# 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

# one can also access methods available on the User class
david = users(:david)
david.call(david.partner)

若要一次取得多個固定裝置,你可以傳入固定裝置名稱清單。例如

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

5 模型測試

模型測試用於測試應用程式的各種模型。

Rails 模型測試儲存在 test/models 目錄下。Rails 提供一個產生器,為你建立模型測試架構。

$ bin/rails generate test_unit:model article title:string body:text
create  test/models/article_test.rb
create  test/fixtures/articles.yml

模型測試沒有自己的超類別,例如 ActionMailer::TestCase。相反地,它們繼承自 ActiveSupport::TestCase

6 系統測試

系統測試讓您測試使用者與應用程式的互動,在真實或無頭瀏覽器中執行測試。系統測試在幕後使用 Capybara。

若要建立 Rails 系統測試,請在您的應用程式中使用 test/system 目錄。Rails 提供一個產生器,為您建立系統測試架構。

$ 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_selector "h1", text: "Users"
  # end
end

預設情況下,系統測試使用 Selenium 驅動程式執行,使用 Chrome 瀏覽器,螢幕大小為 1400x1400。下一部分說明如何變更預設設定。

預設情況下,Rails 會嘗試從測試期間引發的例外中復原,並回應 HTML 錯誤頁面。此行為可以透過 config.action_dispatch.show_exceptions 設定來控制。

6.1 變更預設設定

Rails 讓變更系統測試的預設設定變得非常簡單。所有設定都已抽象化,讓您可以專注於撰寫測試。

當您產生新的應用程式或鷹架時,會在測試目錄中建立一個 application_system_test_case.rb 檔案。這是您所有系統測試設定應儲存的位置。

如果您想要變更預設設定,您可以變更系統測試的「驅動程式」。假設您想要將驅動程式從 Selenium 變更為 Cuprite。首先將 cuprite 寶石新增到您的 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

如果您想要使用無頭瀏覽器,您可以透過在 :using 引數中新增 headless_chromeheadless_firefox 來使用 Headless Chrome 或 Headless Firefox。

require "test_helper"

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

如果您想使用遠端瀏覽器,例如 Docker 中的 Headless 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://127.0.0.1:4444/wd/hub bin/rails test:system

如果您的測試應用程式也在遠端執行,例如 Docker 容器,Capybara 需要更多輸入,說明如何 呼叫遠端伺服器

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  def setup
    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?
    super
  end
  # ...
end

現在您應該可以連線到遠端瀏覽器和伺服器,無論是在 Docker 容器或 CI 中執行。

如果您的 Capybara 組態需要比 Rails 提供的更多設定,可以將此額外組態新增到 application_system_test_case.rb 檔案中。

請參閱 Capybara 的文件,以取得其他設定。

6.2 螢幕截圖輔助工具

ScreenshotHelper 是一個輔助工具,用於擷取測試的螢幕截圖。這有助於在測試失敗時檢視瀏覽器,或稍後檢視螢幕截圖以進行除錯。

提供兩種方法:take_screenshottake_failed_screenshottake_failed_screenshot 會自動包含在 Rails 內的 before_teardown 中。

take_screenshot 輔助工具方法可以包含在測試中的任何位置,以擷取瀏覽器的螢幕截圖。

6.3 實作系統測試

現在我們要為我們的部落格應用程式新增一個系統測試。我們將透過拜訪索引頁面和建立新的部落格文章,來示範撰寫系統測試。

如果您使用腳手架產生器,系統測試架構會自動為您建立。如果您沒有使用腳手架產生器,請先建立一個系統測試架構。

$ 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 來執行所有測試,包括系統測試。

6.3.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」,這會傳送 POST 要求,在資料庫中建立新的文章。

我們會被重新導向回文章索引頁面,然後我們斷言新文章標題的文字在文章索引頁面上。

6.3.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

6.3.3 進一步深入

系統測試的優點在於它類似於整合測試,它測試使用者與您的控制器、模型和檢視的互動,但系統測試更強健,實際上會測試您的應用程式,就像真實使用者在使用它一樣。未來,您可以測試使用者在您的應用程式中會執行的任何動作,例如留言、刪除文章、發佈草稿文章等。

7 整合測試

整合測試用於測試我們應用程式的各個部分如何互動。它們通常用於測試我們應用程式中的重要工作流程。

對於建立 Rails 整合測試,我們使用應用程式的 test/integration 目錄。Rails 提供一個產生器,為我們建立一個整合測試架構。

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

以下是新建立的整合測試的樣子

require "test_helper"

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

這裡的測試繼承自 ActionDispatch::IntegrationTest。這使得我們可以在整合測試中使用一些額外的輔助函式。

預設情況下,Rails 會嘗試從測試期間引發的例外中復原,並回應 HTML 錯誤頁面。此行為可以透過 config.action_dispatch.show_exceptions 設定來控制。

7.1 可用於整合測試的輔助函式

除了標準測試輔助函式之外,繼承自 ActionDispatch::IntegrationTest 還提供一些額外的輔助函式,可用於撰寫整合測試。讓我們簡要介紹我們可以選擇的三種類別輔助函式。

若要處理整合測試執行器,請參閱 ActionDispatch::Integration::Runner

在執行請求時,我們將有 ActionDispatch::Integration::RequestHelpers 可供我們使用。

如果我們需要上傳檔案,請查看 ActionDispatch::TestProcess::FixtureFile 以獲得協助。

如果我們需要修改整合測試的階段或狀態,請查看 ActionDispatch::Integration::Session 以獲得協助。

7.2 實作整合測試

讓我們為部落格應用程式新增整合測試。我們將從建立新部落格文章的基本工作流程開始,以驗證所有功能是否運作正常。

我們將從產生整合測試架構開始

$ 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_select "h1", "Welcome#index"
  end
end

我們將在以下的「測試檢視」區段中查看 assert_select,以查詢請求產生的 HTML 結果。它用於透過斷言關鍵 HTML 元素及其內容的存在來測試我們請求的回應。

當我們造訪我們的根路徑時,我們應該會看到 welcome/index.html.erb 呈現在檢視中。因此,此斷言應該會通過。

7.2.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_select "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!

最後,我們可以斷言我們的回應成功,而且我們的新文章可以在頁面上讀取。

7.2.2 進一步了解

我們能夠成功測試一個非常小的工作流程,用於拜訪我們的部落格並建立一則新文章。如果我們想要進一步了解,我們可以新增測試,用於留言、移除文章或編輯留言。整合測試是針對我們的應用程式實驗各種使用案例的好地方。

8 控制器功能測試

在 Rails 中,測試控制器的各種動作是一種撰寫功能測試的方式。請記住,您的控制器會處理傳入應用程式的 Web 要求,並最終回應已呈現的檢視。在撰寫功能測試時,您會測試您的動作如何處理要求和預期的結果或回應,在某些情況下為 HTML 檢視。

8.1 功能測試中應包含的內容

您應測試諸如

  • Web 要求是否成功?
  • 使用者是否重新導向到正確的頁面?
  • 使用者是否成功驗證?
  • 檢視中是否顯示適當的訊息給使用者?
  • 回應中是否顯示正確的資訊?

查看功能測試運作的最簡單方式是使用腳手架產生器產生控制器

$ bin/rails generate scaffold_controller article title:string body:text
...
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
...

讓我們查看其中一個測試,也就是 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 方法會啟動網路請求,並將結果填入 @response 中。它最多可以接受 6 個參數

  • 您要求的控制器動作的 URI。這可以是字串或路由輔助程式 (例如 articles_url) 的形式。
  • params:選項,其中包含要傳遞到動作的請求參數雜湊 (例如查詢字串參數或文章變數)。
  • headers:用於設定將與請求一起傳遞的標頭。
  • env:用於根據需要自訂請求環境。
  • xhr:請求是否為 Ajax 請求。可以設定為 true 以將請求標記為 Ajax。
  • as:用於使用不同的內容類型對請求進行編碼。

所有這些關鍵字參數都是可選的。

範例:呼叫第一個 Article:show 動作,傳遞 HTTP_REFERER 標頭

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

另一個範例:呼叫最後一個 Article:update 動作,將 title 的新文字傳遞到 params 中,作為 Ajax 請求

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

再一個範例:呼叫 :create 動作來建立新的文章,將 title 的文字傳遞到 params 中,作為 JSON 請求

post articles_path, 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") }

預設情況下,Rails 會嘗試從測試期間引發的例外中復原,並回應 HTML 錯誤頁面。此行為可以透過 config.action_dispatch.show_exceptions 設定來控制。

8.2 功能測試可用的請求類型

如果您熟悉 HTTP 協定,您就會知道 get 是一種請求類型。Rails 功能測試支援 6 種請求類型

  • get
  • post
  • patch
  • put
  • 標頭
  • 刪除

所有要求類型都有您可以使用的等效方法。在典型的 C.R.U.D. 應用程式中,您會更常使用 getpostputdelete

功能測試不會驗證動作是否接受指定的請求類型,我們更關注結果。請求測試存在於此用例中,以讓您的測試更有目的性。

8.3 測試 XHR (Ajax) 請求

若要測試 Ajax 請求,您可以指定 xhr: true 選項給 getpostpatchputdelete 方法。例如

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

8.4 啟示錄的三個雜湊

在請求已提出並處理後,您將有 3 個雜湊物件可供使用

  • cookies - 設定的任何 cookie
  • flash - 存在於快閃記憶體中的任何物件
  • session - 存在於會話變數中的任何物件

與一般的雜湊物件相同,您可以透過參照字串中的金鑰來存取值。您也可以透過符號名稱來參照它們。例如

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

8.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

8.6 設定標頭和 CGI 變數

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

8.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
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
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

8.8 將其組合在一起

在這個時候,我們的 Articles 控制器測試了 :index 以及 :new:create 動作。處理現有資料呢?

讓我們為 :show 動作撰寫測試

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

從我們之前對固定裝置的討論中記住,articles() 方法將讓我們存取我們的 Articles 固定裝置。

刪除現有的 Article 呢?

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

  assert_redirected_to articles_path
end

我們也可以新增一個測試來更新現有的 Article。

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

  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

請注意,我們開始在這些三個測試中看到一些重複,它們都存取相同的 Article 固定裝置資料。我們可以使用 ActiveSupport::Callbacks 提供的 setupteardown 方法來消除重複。

我們的測試現在應該看起來如下所示。暫時忽略其他測試,我們為了簡潔而將它們省略。

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 或方法名稱作為要呼叫的符號來使用。

8.9 測試輔助工具

為避免重複程式碼,你可以新增你自己的測試輔助工具。登入輔助工具可以是一個很好的範例

# 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.9.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 forty two" 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.9.2 急切需要輔助工具

您可能會覺得在 test_helper.rb 中熱切地需要輔助工具很方便,這樣您的測試檔案就能隱式地存取它們。這可以使用全域比對來完成,如下所示

# 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/ 中,或屬於控制器測試的一部分。assert_select 方法允許您使用簡單但強大的語法查詢回應的 HTML 元素。

有兩種形式的 assert_select

assert_select(selector, [equality], [message]) 確保透過 selector 在選取的元素上滿足相等條件。selector 可以是 CSS selector 表達式 (字串) 或具有替代值的表達式。

assert_select(element, selector, [equality], [message]) 確保透過從 element (Nokogiri::XML::NodeNokogiri::XML::NodeSet 的實例) 及其後代開始的 selector 在所有選取的元素上滿足相等條件。

例如,您可以使用以下方式驗證回應中標題元素的內容

assert_select "title", "Welcome to Rails Testing Guide"

您也可以使用巢狀 assert_select 區塊進行更深入的調查。

在以下範例中,針對 li.menu_item 的內部 assert_select 會在外部區塊所選取的元素集合中執行

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

可以反覆處理選取的元素集合,以便針對每個元素分別呼叫 assert_select

例如,如果回應包含兩個順序清單,每個清單有四個巢狀清單元素,則以下測試都會通過。

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

assert_select "ol" do
  assert_select "li", 8
end

此斷言非常強大。如需進階用法,請參閱其文件

10.1 其他基於檢視的斷言

有更多主要用於測試檢視的斷言

斷言 目的
assert_select_email 允許您對電子郵件的內文進行斷言。
assert_select_encoded 允許您對編碼的 HTML 進行斷言。它會取消編碼每個元素的內容,然後呼叫區塊並傳入所有未編碼的元素。
css_select(selector)css_select(element, selector) 傳回由 selector 選取的所有元素的陣列。在第二個變體中,它會先比對基礎 element,然後嘗試比對其任何子項目的 selector 條件式。如果沒有比對,兩個變體都會傳回一個空陣列。

以下是使用 assert_select_email 的範例

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

11 測試檢視部分

部分範本(通常稱為「部分」)是將呈現流程分解成更易於管理的區塊的另一種裝置。使用部分,您可以從範本中萃取程式碼片段到個別檔案,並在範本中重複使用它們。

檢視測試提供了測試部分是否會以您預期的方式呈現內容的機會。檢視部分測試位於 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_selectrails-dom-testing 提供的其他基於檢視的斷言

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

  render "articles/article", article: article

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

為了與 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

如果您的應用程式使用 Ruby >= 3.0 或更高版本,則依賴於 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

從 Action View 版本 7.1 開始,#rendered 輔助方法會回傳一個物件,能夠剖析檢視部分的已呈現內容。

若要將 #rendered 方法回傳的 字串 內容轉換為物件,請透過呼叫 .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

12 測試輔助程式

輔助程式只是一個簡單的模組,您可以在其中定義檢視中可用的方法。

若要測試輔助程式,您只需檢查輔助程式方法的輸出是否符合您的預期。與輔助程式相關的測試位於 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

13 測試您的郵件程式

測試郵件程式類別需要一些特定工具才能徹底完成工作。

13.1 讓郵差保持在檢查中

您的郵件程式類別(就像 Rails 應用程式的其他每個部分一樣)應該經過測試,以確保它們按預期工作。

測試郵件程式類別的目標是確保

  • 電子郵件正在處理(建立和發送)
  • 電子郵件內容正確(主旨、寄件者、內文等)
  • 正確的電子郵件在正確的時間發送

13.1.1 從各方面

測試郵件程式有兩個面向,單元測試和功能測試。在單元測試中,您在嚴格控制的輸入下孤立執行郵件程式,並將輸出與已知值(固定裝置)進行比較。在功能測試中,您不會測試郵件程式產生的細微細節;相反地,我們測試我們的控制器和模型是否以正確的方式使用郵件程式。您測試以證明在正確的時間發送了正確的電子郵件。

13.2 單元測試

若要測試您的郵件程式是否按預期工作,您可以使用單元測試將郵件程式的實際結果與預先寫好的範例進行比較,以了解應該產生什麼。

13.2.1 固定裝置的復仇

為了單元測試郵件寄送器,固定裝置用於提供輸出應有的外觀範例。由於這些是範例電子郵件,而不是像其他固定裝置那樣的 Active Record 資料,因此它們與其他固定裝置分開保存在自己的子目錄中。test/fixtures 中目錄的名稱直接對應於郵件寄送器的名稱。因此,對於名為 UserMailer 的郵件寄送器,固定裝置應駐留在 test/fixtures/user_mailer 目錄中。

如果您產生了郵件寄送器,產生器不會為郵件寄送器動作建立 stub 固定裝置。您必須自行建立這些檔案,如上所述。

13.2.2 基本測試案例

以下是單元測試,用於測試名為 UserMailer 的郵件寄送器,其動作 invite 用於向朋友發送邀請。這是產生器為 invite 動作建立的基礎測試的改編版本。

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # Create the email and store it for further assertions
    email = UserMailer.create_invite("[email protected]",
                                     "[email protected]", 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 ["[email protected]"], email.from
    assert_equal ["[email protected]"], email.to
    assert_equal "You have been invited by [email protected]", email.subject
    assert_equal read_fixture("invite").join, email.body.to_s
  end
end

在測試中,我們建立電子郵件並將傳回的物件儲存在 email 變數中。然後,我們確保它已傳送(第一個 assert),然後在第二批斷言中,我們確保電子郵件確實包含我們預期的內容。輔助函式 read_fixture 用於讀取此檔案中的內容。

當只有一個(HTML 或文字)部分存在時,email.body.to_s 會存在。如果郵件寄送器同時提供兩者,您可以使用 email.text_part.body.to_semail.html_part.body.to_s 針對特定部分測試您的固定裝置。

以下是 invite 固定裝置的內容

Hi [email protected],

You have been invited.

Cheers!

現在正是更深入了解如何為您的郵件寄送器撰寫測試的時機。config/environments/test.rb 中的程式碼行 ActionMailer::Base.delivery_method = :test 將傳遞方法設定為測試模式,以便電子郵件不會實際傳遞(這有助於避免在測試時向您的使用者發送垃圾郵件),而是會附加到陣列(ActionMailer::Base.deliveries)中。

ActionMailer::Base.deliveries 陣列僅會在 ActionMailer::TestCaseActionDispatch::IntegrationTest 測試中自動重設。如果您想要在這些測試案例之外有乾淨的狀態,您可以使用下列指令手動重設:ActionMailer::Base.deliveries.clear

13.2.3 測試已排程的電子郵件

您可以使用 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("[email protected]", "[email protected]")

    # Test that the email got enqueued with the correct arguments
    assert_enqueued_email_with UserMailer, :create_invite, args: ["[email protected]", "[email protected]"] 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: "[email protected]", to: "[email protected]")

    # Test that the email got enqueued with the correct named arguments
    assert_enqueued_email_with UserMailer, :create_invite, args: [{ from: "[email protected]",
                                                                    to: "[email protected]" }] 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("[email protected]", "[email protected]")

    # Test that the email got enqueued with the correct mailer parameters and arguments
    assert_enqueued_email_with UserMailer, :create_invite, params: { all: "good" },
                                                           args: ["[email protected]", "[email protected]"] 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: "[email protected]").create_invite

    # Test that the email got enqueued with the correct mailer parameters
    assert_enqueued_email_with UserMailer.with(to: "[email protected]"), :create_invite do
      email.deliver_later
    end
  end
end

13.3 功能和系統測試

單元測試讓我們可以測試電子郵件的屬性,而功能和系統測試讓我們可以測試使用者互動是否會適當地觸發電子郵件傳遞。例如,您可以檢查邀請朋友操作是否會適當地傳送電子郵件

# 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: "[email protected]" }
    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: "[email protected]"
    assert_emails 1 do
      click_on "Invite"
    end
  end
end

assert_emails 方法與特定傳遞方法無關,且會與使用 deliver_nowdeliver_later 方法傳遞的電子郵件搭配使用。如果我們明確想要斷言電子郵件已排程,我們可以使用 assert_enqueued_email_with上方範例)或 assert_enqueued_emails 方法。您可以在 此處的文件中找到更多資訊。

14 測試工作

工作可以在隔離狀態(專注於工作的行為)和情境中(專注於呼叫程式碼的行為)進行測試。

14.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,因此如果設定了重試,重試失敗會被測試捕捉,而不是重新加入佇列並忽略。

14.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

14.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
    refute account.reload.charged_for?(product)
  end
end

一般不建議使用這個方法,因為它會繞過框架的某些部分,例如引數序列化。

15 測試 Action Cable

由於 Action Cable 在應用程式內部不同層級使用,您需要測試頻道、連線類別本身,以及其他實體是否廣播正確的訊息。

15.1 連線測試案例

預設情況下,當您使用 Action Cable 產生新的 Rails 應用程式時,也會在 test/channels/application_cable 目錄下產生一個基本連線類別 (ApplicationCable::Connection) 的測試。

連線測試旨在檢查連線識別碼是否正確指派,或任何不當的連線要求是否遭到拒絕。以下是範例

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 文件,以取得更多資訊。

15.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

這個測試相當簡單,只會斷言頻道將連線訂閱到特定串流。

您也可以指定底層連線識別碼。以下是 Web 通知頻道的範例測試

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 文件,以取得更多資訊。

15.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

16 測試 Eager Loading

通常,應用程式不會在 developmenttest 環境中 eager load 以加快速度。但它們會在 production 環境中進行 eager load。

如果專案中的某些檔案因任何原因而無法載入,你最好在部署到生產環境之前偵測到,對吧?

16.1 持續整合

如果你的專案已設定 CI,在 CI 中進行 eager loading 是確保應用程式進行 eager loading 的一種簡單方法。

CI 通常會設定一些環境變數來表示測試套件在那裡執行。例如,它可能是 CI

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

從 Rails 7 開始,預設會以這種方式設定新產生的應用程式。

16.2 裸測套件

如果你的專案沒有持續整合,你仍然可以透過呼叫 Rails.application.eager_load! 在測試套件中進行 eager load。

16.2.1 Minitest

require "test_helper"

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

16.2.2 RSpec

require "rails_helper"

RSpec.describe "Zeitwerk compliance" do
  it "eager loads all files without errors" do
    expect { Rails.application.eager_load! }.not_to raise_error
  end
end

17 其他測試資源

17.1 測試時間相關程式碼

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 參考文件,以取得有關可用時間輔助方法的更多資訊。

回饋

我們鼓勵你協助提升本指南的品質。

如果你看到任何錯字或事實錯誤,請協助我們修正。要開始進行,你可以閱讀我們的 文件貢獻 章節。

您也可能會發現內容不完整或未更新。請務必新增任何 main 遺失的文件。請務必先查看 Edge Guides,以驗證問題是否已在主分支中修復。查看 Ruby on Rails Guides Guidelines 以了解樣式和慣例。

如果您發現需要修復但無法自行修補的任何原因,請 開啟問題

最後但並非最不重要的一點是,歡迎在 官方 Ruby on Rails 論壇 上討論有關 Ruby on Rails 文件的任何問題。