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
helpers
、mailers
和 models
目錄分別用於存放檢視輔助程式、郵件和模型的測試。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_password
和 test_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_method
和 send
呼叫才能正常運作,但正式來說,名稱的限制很少。
接下來,我們來看我們的第一次斷言
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] ) |
確保 obj 為 empty? 。 |
assert_not_empty( obj, [msg] ) |
確保 obj 不是 empty? 。 |
assert_match( regexp, string, [msg] ) |
確保字串符合正規表示式。 |
assert_no_match( regexp, string, [msg] ) |
確保字串不符合正規表示式。 |
assert_includes( collection, obj, [msg] ) |
確保 obj 在 collection 中。 |
assert_not_includes( collection, obj, [msg] ) |
確保 obj 不在 collection 中。 |
assert_in_delta( expected, actual, [delta], [msg] ) |
確保數字 expected 和 actual 在彼此 delta 範圍內。 |
assert_not_in_delta( expected, actual, [delta], [msg] ) |
確保數字 expected 和 actual 不在彼此 delta 範圍內。 |
assert_in_epsilon ( expected, actual, [epsilon], [msg] ) |
確保數字 expected 和 actual 的相對誤差小於 epsilon 。 |
assert_not_in_epsilon ( expected, actual, [epsilon], [msg] ) |
確保數字 expected 和 actual 的相對誤差不大於 epsilon 。 |
assert_throws( symbol, [msg] ) { block } |
確保給定的區塊會擲出符號。 |
assert_raises( exception1, exception2, ... ) { block } |
確保給定的區塊會引發其中一個給定的例外。 |
assert_instance_of( class, obj, [msg] ) |
確保 obj 是 class 的實例。 |
assert_not_instance_of( class, obj, [msg] ) |
確保 obj 不是 class 的實例。 |
assert_kind_of( class, obj, [msg] ) |
確保 obj 是 class 的實例或繼承自 class 。 |
assert_not_kind_of( class, obj, [msg] ) |
確保 obj 不是 class 的實例或未繼承自 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) 為 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 提供了以下類別供您繼承
ActiveSupport::TestCase
ActionMailer::TestCase
ActionView::TestCase
ActiveJob::TestCase
ActionDispatch::IntegrationTest
ActionDispatch::SystemTestCase
Rails::Generators::TestCase
這些類別每個都包含 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-0
和 test-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.rb
或 db/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.png
有 image/png 編碼的檔案,下列 YAML 固定裝置條目將產生相關的 ActiveStorage::Blob
和 ActiveStorage::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
目錄的固定裝置。載入包含三個步驟
- 從與固定裝置對應的資料表中移除任何現有資料
- 將固定裝置資料載入資料表中
- 將固定裝置資料傾印到方法中,以防你想要直接存取它
為了從資料庫中移除現有資料,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_chrome
或 headless_firefox
來使用 Headless Chrome 或 Headless Firefox。
require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :headless_chrome
end
如果您想使用遠端瀏覽器,例如 Docker 中的 Headless Chrome,您必須新增遠端 url
並透過 options
將 browser
設定為遠端。
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_screenshot
和 take_failed_screenshot
。take_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. 應用程式中,您會更常使用 get
、post
、put
和 delete
。
功能測試不會驗證動作是否接受指定的請求類型,我們更關注結果。請求測試存在於此用例中,以讓您的測試更有目的性。
8.3 測試 XHR (Ajax) 請求
若要測試 Ajax 請求,您可以指定 xhr: true
選項給 get
、post
、patch
、put
和 delete
方法。例如
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
- 設定的任何 cookieflash
- 存在於快閃記憶體中的任何物件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 變數
# 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
提供的 setup
和 teardown
方法來消除重複。
我們的測試現在應該看起來如下所示。暫時忽略其他測試,我們為了簡潔而將它們省略。
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 中的其他回呼,setup
和 teardown
方法也可以透過傳遞區塊、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/lib
或 test/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::Node
或 Nokogiri::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_select
和 由 rails-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
會定義一個剖析器,用於
:html
- 回傳一個 Nokogiri::XML::Node 執行個體。:json
- 回傳一個 ActiveSupport::HashWithIndifferentAccess 執行個體。
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_to
或 pluralize
。
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_s
或 email.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::TestCase
和 ActionDispatch::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_now
或 deliver_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_jobs
和 perform_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
通常,應用程式不會在 development
或 test
環境中 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 文件的任何問題。