Rails 應用測試指南
rails 英文文章翻譯
DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.
Rails 應用程式測試
此頁教學包含了Rails中,用於測試應用程式的機制
等閱讀完本篇教學後,你將會學到:
- Rails的測試術語
- 如何為你的系統寫單元測試、功能測試、整合測試和系統測試
- 其他受歡迎的測試方法、插件
1、為何什麼要為你的Rails應用程式寫測試呢?
Rails 讓編寫測試變得非常容易。當你創造models、controllers的時候,就會順便建立一個框架測試代碼。
藉由運行測試,你可以確保你的程式碼,即使在主要程式碼重構之後,也可以保證功能一樣的正常運行。
Rails 還可以模擬瀏覽器的requests,也因此讓您無需通過瀏覽器,就可以測試應用程式的response。
2、測試介紹
在一開始,支援測試就已經被編寫入Rails的結構之中,不是因為「哇!因為測試是新的酷東西,所以我們把測試加進去」
2.1 Rails 開始進行測試
當你用rails new application_name
的時候,Rails會馬上幫你產生一個 test
的資料夾,如果你列出目錄的話,你將會看到:
$ ls -F test
application_system_test_case.rb controllers/ helpers/ mailers/ system/
channels/ fixtures/ integration/ models/ test_helper.rb
system
系統測試資料夾代表系統測試,他被使用於對應用程式進行完整的瀏覽器測試。系統測試可以讓你以使用者體驗的角度,去測試你的應用程式,並幫助你測試 JavaScript。系統測試繼承於 Capybara,並在你應用程式的瀏覽器測試中執行
Fixtures
夾具是一種管理測試數據的方式,會在 fixtures
這個資料夾裡面。
jobs
資料夾將會在第一個關聯測試被產生的時候,順便新建一個出來
test_helper.rb
檔案中包含測試的預設設定。
application_system_test_case.rb
檔案中包含了系統測試的預設設定。
2.2 測試環境
每個Rails應用程式,預設下都有三個環境:開發、測試、生產環境。
每個環境設置都可以被修改,以這個為範例,我們可以藉由改變 config/environments/test.rb
的選項,來修改測試環境。
備註:運行測試時,RAILS_ENV環境變量的值是test,Ex. RAILS_ENV=test
。
2.3 使用 Minitest 測試 Rails 應用程式
如果你記得,我們在 Rails 入門 這個教學中,使用 bin/rails generate model
這個指令來創造我們第一個Model,這個指令會創造很多東西,其中包括在 test
資料夾中創建test stubs
。
$ 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
中,預設的 test 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
因為 ArticleTest
繼承自 ActiveSupport::TestCase
,所以此類別也有定義一個測試用例(test case),因此 ArticleTest
具有 ActiveSupport::TestCase
中所有可以用的方法。在本教學後面,我們將會看到他為我們提供的一些方法。
任何繼承自 Minitest::Test
(他是 ActiveSupport::TestCase
的 superclass) 而且名稱開頭為 test_
的方法,都簡稱為測試。
因此,名為 test_password
和 test_valid_password
兩個方法,都是有效的測試名稱,並且當測試用例運行時,會自動運行。
Rails 還有一個 test
方法,他可以接受測試一個名稱、區塊。他會生成一個名稱前綴為 test_
的 Minitest::Unit
測試。所以你不必擔心命名方法,你可以這樣寫:
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
讓我們來對這個新增的行數進行測試(..rb: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
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
代表失敗。你可以先看到相對應失敗測試的名稱,接下來幾行包含斷言的實際值、預期值得訊息。
預設的斷言訊息的訊息,足以幫助我們找到錯誤,為了讓斷言的錯誤訊息更具可讀性,每個斷言提供一些參數選項做修改,如下:
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
現在,為了讓這個測試通過,我們可以幫此model的 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
現在,如果你有注意到,我們第一個寫的測試,他沒有達到預期的功能性,然後我們加了一些程式碼來添加功能,最後確保測試可以通過。這種開發流程就被稱作為測試驅動開發(Test-Driven Development (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>'
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
這個輸出,他代表這個測試是錯的
Notice the ‘E’ in the output. It denotes a test with error.
備註:只要有任何錯誤,或者是斷言失敗發生,測試的執行就會停止,並且測試套件會繼續執行下一個方法。所有的測試方法是隨機去執行的,不過可以使用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
是可新增的字串訊息,可以使用它讓你的測試失敗訊息更易讀。
Assertion | Purpose |
---|---|
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 是 空的 。 |
assert_not_empty( obj, [msg] ) |
確保 obj 不是 空的 。 |
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 或其子代的實體。 |
assert_not_kind_of( class, obj, [msg] ) |
確保 obj 不是 class 或其子代的實體。 |
assert_respond_to( obj, symbol, [msg] ) |
確保 obj 能回應 symbol 對應的方法。 |
assert_not_respond_to( obj, symbol, [msg] ) |
確保 obj 不能回應 symbol 對應的方法。 |
assert_operator( obj1, operator, [obj2], [msg] ) |
確保 obj1.operator(obj2) 成立。 |
assert_not_operator( obj1, operator, [obj2], [msg] ) |
確保 obj1.operator(obj2) 不成立。 |
assert_predicate ( obj, predicate, [msg] ) |
確保 obj.predicate 是真, 像是: assert_predicate str, :empty? |
assert_not_predicate ( obj, predicate, [msg] ) |
確保 obj.predicate 是假, 像是: assert_not_predicate str, :empty? |
flunk( [msg] ) |
確保失敗,可以用這個斷言來明確標示未完成的測試。 |
以上是minitest支援的斷言子集,如果想要看更詳細的列表,可以看這個 Minitest API documentation, 尤其是 Minitest::Assertions
.
因為測試框架模塊化的特性,你可以自己創造斷言,事實上,這是Rails所做的,他做了一些斷言讓你寫測試更輕鬆。
備註:創建自己的斷言是進階主題,我們在這邊不會教到。
2.5 Rails 特定的斷言
Rails將自己一些自定義的斷言加進 minitest
這個框架中:
Assertion | Purpose |
---|---|
assert_difference(expressions, difference = 1, message = nil) {...} |
運行代碼區塊前後數量變化了多少(通過expression 表示)。 |
assert_no_difference(expressions, message = nil, &block) |
運行代碼區塊前後數量沒變化(通過expression 表示)。 |
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) |
斷言回應的狀態程式碼, 可以指定表示200-299 的:success,表示300-399 的:redirect,表示404 的:missing,或者表示500-599 的:error。 此外,還可以明確指定數字狀態碼或對應的符號。詳情參見完整的狀態碼列表及其與符號的對應關係。完整狀態程式碼 和 對應關係。 |
assert_redirected_to(options = {}, message=nil) |
斷言傳入的重導選項匹配最近一個動作中的重定向。 你可以傳路徑,像是 assert_redirected_to root_path 和 Active Record 物件,像是 assert_redirected_to @article . |
你將會在下一章節看到一些斷言的用法。
2.6 關於測試用例的簡短說明
我們在自己的測試用例中,可以使用繼承自 Minitest::Assertions
類別的所有基本斷言,像是 assert_equal
。事實上,Rails以下類別都是繼承自 Minitest::Assertions
:
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
這會在這個測試用例中,執行所有的測試方法。 This will run all test methods from the test case.
你可以藉由輸入 -n
或是 --name
加上方法名稱,來執行該測試用例中的特定測試方法
You can also run a particular test method from the test case by providing the
-n
or --name
flag and the test’s method 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/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 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.9 在持續整合中執行測試 (CI)
在持續整合的環境中執行測試,只需要輸入一行指令: To run all tests in a CI environment, there’s just one command you need:
bin/rails test
如果你想要使用 系統測試,要記得只輸入 bin/rails test
不會執行系統測試,因為這樣會讓系統變很慢,如果想要讓他們正常執行,只要輸入 bin/rails test:system
或是改變第一步驟的指令 bin/rails test:all
,這樣就可以讓你在執行測試的時候,把系統測試包含在裡面。
3、平行測試
平行測試允許你平行你的測試套件,雖然分叉進程是預設方法,但是單線進程也是支持的,平行測試可以讓你減少測試執行的時間。
3.1 執行平行測試
Rails預設的平行方法,是使用DRb系統來運作的。這些進程根據有多少數量的工人來進行分叉。預設的數量是根據你電腦所擁有的核心數,不過可以透過平行方法並給予參數來改變數量。
在 test_helper.rb
此檔案裡面,可以找到平行方法:
class ActiveSupport::TestCase
parallelize(workers: 2)
end
有多少個工人數量,就代表進程會被分叉幾次,你可能希望用不同於持續整合(CI)的方式,平行本地測試插件,因此有一個環境變數提供你去更改測試的工人數量:
$ PARALLEL_WORKERS=15 bin/rails test
當平行測試時,Active Record會自動處理創建數據庫,並加載schema到每個數據庫的進程之中。數據庫將會以工人相對應的編號作為後綴。例如,如果你在這個測試有兩個工人,這個測試會分別創造 test-database-0
和 test-database-1
。
如果工人的數量是1個或是低於進程數量,就不會分叉進行,測試也不會進行平行測試,資料庫也會使用原本的 test-database
。
有提供兩個 hooks,一個是在進程分叉時運行,一個是在分叉進程關閉前運行,如果你的應用程式使用多個資料庫,或者執行其他取決於工作人員數量的任務,這會非常的有用。
進程被分叉後,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 測試平行交易(Transactions)
Rails會自動將所有的測試用例,包裝在測試完成後回滾(rollback)的資料庫交易(transaction)中,這會讓測試用例彼此獨立,並且對資料庫的更改只有在單個測試中可以看到。
當你想要測試在執行緒中執行平行交易(transactions)的程式碼時,交易(transactions)可以互相阻擋,因為他們已經鑲嵌在測試交易(transactions)下面。
你可以透過設定 self.use_transactional_tests = false
,來關閉測試用例類別中的交易(transactions)。
class WorkerTest < ActiveSupport::TestCase
self.use_transactional_tests = false
test "parallel transactions" do
# start some threads that create transactions
end
end
標註:對於被禁止使用的交易(transactional)測試,你必須去清除任何測試新增的任何數據,因為測試完成後,更改不會自動回滾(rollback)
3.4 平行化測試的閾值
平行測試會增加數據庫設定和夾具(fixture)加載方面的負載。因此Rails不會平行執行少於50個測試的執行。
你可以在 test.rb
設定你的閾值:
config.active_support.test_parallelization_threshold = 100
你也可以在測試用例的地方設定閾值:
class ActiveSupport::TestCase
parallelize threshold: 100
end
4、測試資料庫
每個Rails的應用程式,幾乎都會與資料庫進行大量的交互運作,因此你的測試也會需要與資料庫進行交互運作,為了要寫更高效率的測試,你需要了解如何設定資料庫、使用樣本數據填充他。
預設下,每個Rails的應用程式有三個環境:開發、測試、生產(production),每個數據庫都在 config/database.yml
中設定。
專用測試資料庫允許你單獨設定測試數據並和他交互使用,這樣可以讓你很放心的處理測試資料,不用擔心會影響到開發、生產(production)的資料庫。
4.1 維護測試資料庫的架構(Schema)
為了執行測試,你的測試資料庫需要有目前數據庫的架構,test helper
會確認你目前是否有任何的 pending migrations
,他會嘗試加載你的 db/schema.rb
、db/structure.sql
匯入測試資料庫。如果 migrations
還有 pending
狀態,會引發錯誤,通常這代表 schema
尚未完全的 migrate
。執行 bin/rails db:migrate
會保持 schema
保持最新狀態。
備註: 如果對現有的 migrations
進行修改,測試資料庫就需要重新建立,可以執行 bin/rails db:test:prepare
來做到這件事。
4.2 夾具(Fixtures)詳解
一個良好的測試,需要考慮測試數據的設定,在Rails,你可以通過定義、客製夾具(fixtures)來處理這個需求。你可以在 Fixtures API documentation 找到全面的文件。
4.2.1 什麼是夾具(Fixtures)?
_Fixtures_
是樣本數據中特別的單字,夾具(Fixtures)允許你在測試開始前,先把預先定義的數據,填充到測試資料庫裡面,夾具(Fixtures)獨立於資料庫,並且用YAML書寫。在test資料夾,每個 Model 都有一個個對應的夾具(Fixtures)檔案。
備註:夾具(Fixtures)不是來創造你測試需要的每個物件,需要用到公用的預設數據時才應該使用。
你可以在 test/fixtures
這個資料夾下面找到夾具(fixtures),當你執行 bin/rails generate model
創造model的時候,Rails會自動在這個資料夾裡面創建 fixture stubs
。
4.2.2 YAML
YAML格式夾具(fixtures)是一個描述你的樣本資料人性化的方式,這個格式的副檔名是 .yml (像是 users.yml
)。
下面是YAML的樣本夾具(fixture)檔案
# 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
每個夾具(fixture)被賦予一個名稱,該名稱後面跟著冒號分隔著的鍵/值的縮進列表。記錄通常有空格來做分行,你可以在第一行用 # 字號的方式把註解加進夾具(fixture)檔案中。
如果你正在運行關聯,你可以在兩個夾具(fixtures)間定義一個參考節點,這裡有個 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
裡面,可以找到 first
裡面有一個鍵值是 category
/about
的,還有 fixtures/action_text/rich_texts.yml
裡的 first_content
有一個鍵值是 record
/first (Article)
的,這代表Active Record會為前者加載在 fixtures/categories.yml
中的 about
裡面,為後者加載在 fixtures/articles.yml
中的 first
裡面。
備註: 在夾具(features)中創建關聯時,引用的是另一個夾具(features)的名稱,而不是 id:
屬性,Rails將會自動分配一個主鍵以便在運行間保持一致,有關關聯行為的更多資訊,請閱讀這個文件Fixtures API documentation。
4.2.3 文件附件夾具(Fixtures)
就跟其他Active Record支持的models一樣,Active Storage附件紀錄繼承自 ActiveRecord::Base
實體,因此可以由夾具(Fixtures)填充。
考慮 Article
的model,他跟圖片有一個 thumbnail
附件的關聯,以及夾具(Fixtures)數據YAML:
class Article
has_one_attached :thumbnail
end
# test/fixtures/articles.yml
first:
title: An Article
假設在 test/fixtures/files/first.png
中有一個 image/png 的編碼文件,下面的YAML夾具條目(YAML fixture entries)將會產生跟 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加強夾具(feature)
ERB檔案可以讓你透過範例鑲嵌Ruby的程式碼,當Rails加載夾具(Fixtures)的時候,YAML夾具(Fixtures)格式會用ERB來做預先處理。這可以讓你使用Ruby語法來幫助你產生一些樣本檔案,例如,下面的程式碼會生成1000個使用者。
<% 1000.times do |n| %>
user_<%= n %>:
username: <%= "user#{n}" %>
email: <%= "user#{n}@example.com" %>
<% end %>
4.2.5 夾具(Fixtures) in Action
預設情況下,Rails會自動從 test/fixtures
資料夾載入所有的夾具(fixtures),載入包含三個步驟:
- 從夾具(fixture)相對應的資料表(table)中,刪除已經存在的資料
- 把夾具資料(fixture data)載入資料表(table)
- 將夾具資料存在方法中,以便你直接去只用他
提示:為了防止從資料庫刪除現存的資料,Rails嘗試禁止引用完整觸發器(像是外鍵、約束檢查),如果你在跑測試的時候遇到煩人的驗證錯誤,確保資料庫的使用者有權限在測試環境中禁用這些觸發器。(在PostgreSQL,只有超級用戶可以禁用所有觸發器,閱讀更多的PostgreSQL權限設定here)。)
4.2.6 夾具(Fixtures) 是 Active Record 的物件
夾具(Fixtures)是 Active Record
的實體,正如上面的第三點所述,你可以直接使用這個物件,因為夾具(feature)中的數據會轉儲成測試用例作用域中的方法。例如:
# 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)
如果想要一次獲得多個夾具(fixtures),你可以傳入多個夾具(fixture)名稱,例如:
# this will return an array containing the fixtures david and steve
users(:david, :steve)
5、模型測試 Model Testing
Model測試用於測試你應用程式中的多種models。
Rails model 測試被存於 test/models
資料夾的下方,Rails提供一個生成器來幫助你創造model測試的框架。
$ bin/rails generate test_unit:model article title:string body:text
create test/models/article_test.rb
create test/fixtures/articles.yml
Model測試沒有像 ActionMailer::TestCase
有自己的 superclass
,相反的,他們繼承自ActiveSupport::TestCase
。
6、系統測試 System Testing
系統測試允許你讓你測試使用者和你的應用程式進行互動,是在真實或無頭瀏覽器中進行測試,系統測試在底層使用 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 driver
運行,並且使用Chrome瀏覽器,螢幕大小為 1400X1400,下一個部分會解釋如何更改預設設定。
6.1 改變預設設定
Rails讓改變系統測試的預設設定變得很簡單,所有設定都被抽象出來,因此你可以專注於寫測試。
當妳生成一個應用程式或者是 scaffold
時,會在測試資料夾創造application_system_test_case.rb
這個檔案,這是所有你的系統設定都會有的。
如果你想要改變預設設定,你可以改變系統測試是由什麼來驅動(driven by),假設你想由 Selenium
改成 Cuprite
驅動,首先加這個 cuprite
gem到gemfile,然後到 application_system_test_case.rb
這個檔案加下面這一句:
require "test_helper"
require "capybara/cuprite"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :cuprite
end
驅動名稱 driven_by
是必要新增的參數,而 driven_by
的參數傳給瀏覽器是用於 :using
(這個只有Selenium使用), :screen_size
是改變螢幕大小,:options
是被用來設定驅動支援的選項。
require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :firefox
end
如果你想要使用無頭瀏覽器,你可以用 在 :using
引數的地方加上 headless_chrome
或是 headless_firefox
,這樣可以新增無頭Chrome或者是無頭Firefox瀏覽器。
require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :headless_chrome
end
如果你想要使用遠端瀏覽器,像是Headless Chrome in Docker,你必須透過 options
使用遠端 url
。
require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
options = ENV["SELENIUM_REMOTE_URL"].present? ? { url: ENV["SELENIUM_REMOTE_URL"] } : {}
driven_by :selenium, using: :headless_chrome, options: options
end
像這樣的案例, webdrivers
這個gem就不再需要required,你應該需要完全移除他,或是在 Gemfile
那邊增加 require:
# ...
group :test do
gem "webdrivers", require: !ENV["SELENIUM_REMOTE_URL"] || ENV["SELENIUM_REMOTE_URL"].empty?
end
現在應該可以成功連結遠端瀏覽器。
$ SELENIUM_REMOTE_URL=http://localhost:4444/wd/hub bin/rails test:system
如果你的應用程式在測試的時候是跑遠端,像是 Docker container
, Capybara
需要更多關於 call remote servers 的資訊。 s
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 container
或是在 CI
中執行。
如果今天你的 Capybara
設定比Rails還要多,可以把額外設定都加進 application_system_test_case.rb
這個檔案裡面。
請看 Capybara’s documentation 有更多的額外設定。
6.2 截圖幫手
ScreenshotHelper
是一個被設計來幫你的測試捕捉截圖的小幫手,這有助於測試失敗時看到瀏覽器的畫面,或是查看截圖來debugging。
這邊提供兩個方法: take_screenshot
和 take_failed_screenshot
, take_failed_screenshot
是自動包含在Rails裡面的 before_teardown
, take_screenshot
輔助方法可以包含在你測試中的任何位置,以擷取瀏覽器的畫面。
6.3 執行系統測試
現在我們來幫我們的部落格應用程式加系統測試,我們將會拜訪索引頁面(index page)和創造新的部落格文章,來展示如何編寫系統測試。
如果使用 scaffold generator
,系統測試框架會自動產生給你,如果你沒有使用,可以跟著下一面寫來產生框架。
$ bin/rails generate system_test articles
之後會創造測試檔案給我們,終端機的輸出指令會是下面這樣:
invoke test_unit
create test/system/articles_test.rb
現在讓我打開檔案並且寫下我們第一行的斷言assertion:
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
,這會讓測試進到文章的 index page
,然後這個 click_on "New Article"
將會找到在 index page
頁面中,”New Article”的按鈕,這會重導到瀏覽器 /articles/new
的連結。
然後這一行 click_on "New Article"
將會找到在 index page
頁面中,”New Article”的按鈕,這會重導到瀏覽器 /articles/new
的連結
再來測試將會用指定的文字填進文章中的標題和內容,一旦欄位被填寫,”Create Article” 被點擊,將會送出 Post request
,並且在資料庫中新增一個新文章。
最後我們將會重導到文章的 index page
,並且我們斷言在文章標題的文字,會在文章中的 index page
裡面。
6.3.2 在不同大小的螢幕測試
如果想要在電腦測試時,多測試不同畫面大小的狀況,你可以創造另外一個繼承自 SystemTestCase
而且在測試插件中使用的類別,在這個範例裡面,以下的設定會在 /test
資料夾裡面,新增 mobile_system_test_case.rb
的檔案
require "test_helper"
class MobileSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :chrome, screen_size: [375, 667]
end
使用這個設定,創造一個繼承自 MobileSystemTestCase
且在 test/system
的測試,現在你可以在多個不同的螢幕大小測試應用程式。
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 更進一步
系統測試美妙的地方就在於他跟整合測試很像,他們都在測試使用者在你的controller、model、view做互動,不過系統測試更完整一點,他會實際上像真正的使用者在使用他一樣,測試你的應用程式,進一步看,你可以測試任何,使用者在應用程式中做的事情,像是評論、刪除文章、刪除草稿文章…等等。
7、整合測試 Integration Testing
整合測試使用在應用程式中各個部分如何互相互動,他們通常用在測試應用程式的重要工作流程。
為了新增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
,這可以讓我們為整合測試新增一些額外有用的方法。
7.1 用在整合測試的小幫手
除了標準的測試小幫手外,還有從 ActionDispatch::IntegrationTest
繼承來一些可以用來編寫整合測試的小幫手,我們來簡單介紹一下可以選擇的三種類型小助手。
有關處理執行整合測試,請看這一篇ActionDispatch::Integration::Runner
。
當執行請求時(performing requests),我們會有 ActionDispatch::Integration::RequestHelpers
這個可以使用。
如果我們需要修改工作階段(session)或者是整合測試的狀態,可以看這一篇尋求幫助 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
我們會在”測試畫面 Testing Views”之下,去找 assert_select
查詢請求的結果HTML,他藉由斷言關鍵的HTML元素、內容來測試我們請求的回應。
當我們拜訪(visit)根目錄,我們應該會看到 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 controller
的 :new
動作。應該得到成功的回應。
然後,我們向 Articles controller
的 :create
動作發送 POST 請求:
post "/articles",
params: { article: { title: "can create", body: "article successfully." } }
assert_response :redirect
follow_redirect!
這兩行的意思是,當我們創造新的文章,會有一個處理重導的設置。
標註:如果你想要在重導後發出後續的請求,別忘了呼叫 follow_redirect
。
最後我們可以斷言我們的回應是成功的,而且頁面中顯示了新建的文章。
7.2.2 更進一步
我們剛剛測試了訪問部落格和新建文章功能,這只是工作流程的一小部分。如果想更進一步,還可以測試評論、刪除文章或編輯評論。整合測試就是用來檢查應用的各種使用場景的。
8、控制器功能測試 Functional Tests for Your Controllers
在Rails,測試控制器各動作需要編寫功能測試,記得你的 controller
處理你應用程式即將到來的網站請求,並且最後回應、渲染到頁面上,當你寫功能測試時,你正在測試如何處理請求和預期結果或回應(在某些情況下是 HTML view
)。
8.1 功能測試應該包含哪些內容
你應該測試的內容有這些:
- 網站請求是否有成功?
- 使用者是否被重導到正確的頁面?
- 使用者是否成功通過身份驗證?
- 是否在
view
中成功顯示了適當的訊息? - 回應中是否有顯示正確的資訊?
查看功能測試最簡單的方式,就是使用框架生產器來產生 controller
:
$ 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
...
這會產生 controller code
和測試 Article
的資源。你可以查看在 test/controllers
資料夾中的 articles_controller_test.rb
文件。
如果你已經有 controller
並且只想產生7個測試框架的程式碼給7個預設的 actions
,你可以輸入下面的程式碼。
$ 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模擬 action
是 index
的請求,確認請求成功並且確保可以生成正確的回應主體。
get
方法發起網站請求,並且把結果傳到 @response 回應裡面
,他最多可以接受六個引數:
- 你請求
controller action
的URI,他可以是字串的形式或是路徑小幫手(e.g.articles_url
)。 params
:帶有參數請求hash
的選項可以傳遞給action
。headers
:用於會隨著請求傳遞的標頭env
:用於根據需要自定義請求環境xhr
:請求是否為Ajax
請求,可以設定為true來幫請求標記為Ajax
as
:用於對不同類型的內容作編碼的請求
這些的關鍵字引數都是可選擇的。
例如:第一個 Article
呼叫 :show
的 action
,傳入 HTTP_REFERER
的標頭:
get article_url(Article.first), headers: { "HTTP_REFERER" => "http://example.com/home" }
另一個例子:為最後的 Article
呼叫 :update
的 action
,傳進新的文字,用 Ajax
請求,在 params
為 title
傳進新的文本:
patch article_url(Article.last), params: { article: { title: "updated" } }, xhr: true
再一個例子: 呼叫 :create
的 action
來創造新的文章,用 JSON
請求,在 params
為 title
傳進新的文本:
post articles_path, params: { article: { title: "Ahoy!" } }, as: :json
備註: 如果嘗試從 articles_controller_test
執行 test_should_create_article
測試,他會因為新添加的 model
級別驗證,導致測試失敗,這是正確的。
讓我們來修改 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") }
8.2 功能測試中的請求類型
如果你熟悉HTTP協議的話,會知道 get
是一種請求類型,Rails功能測試中可以使用六種請求:
get
post
patch
put
head
delete
這幾種請求都要相對應的方法使用,在常見的CRUD中,最常使用的是 get
、 post
、 put
和 delete
。
備註: 功能測試不驗證 action
是否接受指定的請求類型,我們關注的是請求的結果,如果想要做到這樣的測試,可以做請求測試(request test)。
8.3 測試 XHR (Ajax) 請求
測試 Ajax 請求,可以在 get
、 post
、 patch
、 put
和 delete
方法中設定 xhr: true
選項,例如:
test "ajax request" do
article = articles(:one)
get article_url(article), xhr: true
assert_equal "hello world", @response.body
assert_equal "text/javascript", @response.media_type
end
8.4 可用的三個HASH
請求發送並處理之後,你會有三個 hash
物件可以使用:
cookies
- 設定cookiesflash
- 任何在flash
中的物件session
- 任何在工作階段變數的物件
和普通 HASH
物件一樣,你可以使用字串的鍵值來獲取相對應的值,另外,你也可以使用符號來獲得值,例如:
flash["gordon"] flash[:gordon]
session["shmession"] session[:shmession]
cookies["are_good_for_u"] cookies[:are_good_for_u]
8.5 可用的實體變數
在 request
之後,功能測試你一樣可以使用三個實體變數:
@controller
- 處理請求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 headers 和 CGI variables 可以透過 headers
參數傳入:
# 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
訊息
如果你記得前面說的,在三個 hash
中有一個是 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
讓我們來增加 flash
訊息到我們的 controller
,加上去後,我們的 :create action
會看起來像這樣:
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 測試其他的 action
到現在為止,我們測試了文章 controller
的 :index
、 :new
、 :create
,我們要怎麼處理現有的數據?
我們來寫一下 :show
的測試:
test "should show article" do
article = articles(:one)
get article_url(article)
assert_response :success
end
還記得我們前面對夾具(fixtures)的討論嗎? 我們可以使用 articles()
方法去使用 Articles 的夾具(fixtures)。
那如何刪除已經存在的文章呢?
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
我們來增加一個更新已經存在的測試。
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
注意我們在這三個測試中開始看到有重複的地方了,他們都用了同一個夾具(fixture)的數據,避免重複,我們可以使用 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其他的 callbacks
一樣, setup
和 teardown
方法都可以支援區塊、lambda、符號形式的方法命名。 s
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
積極的引入小幫手,這樣就可以很方便的找到他,因此你的測試檔案可以很隱晦地去使用它們,這邊可以使用 globbing
來完成,就像下面:
# test/test_helper.rb
Dir[Rails.root.join("test", "test_helpers", "**", "*.rb")].each { |file| require file }
不過這樣會有增加啟動時間的缺點。因為不是在你個人測試中手動引入需要的檔案。
9、測試路徑 Testing Routes
就像跟其他Rails應用程式一樣,你可以測試路徑,路徑測試在 test/controllers/
或者部分在 controller
測試裡面。
備註: 如果你的應用程式有很複雜的路徑,Rails提供一個很好用的測試小幫手來測試他們。
關於Rails中,更多可用的路徑斷言,可以看這個API文件 ActionDispatch::Assertions::RoutingAssertions
。
10、測試畫面 Testing Views
測試請求中,藉由斷言(assert)關鍵HTML元素的存在和內容,是一個在你測試應用程式畫面(views)中很常見的方式,就像路徑測試,畫面(views)測試在 test/controllers/
,或者一部分在 controller
測試裡面, assert_select
,這個方法允許你查詢藉由使用簡單且強大的語法,去查詢回應的HTML元素。
有兩種形式的 assert_select
:
assert_select(selector, [equality], [message])
測試選擇器所選的元素是否符合 equality
指定條件, selector
可以是CSS選擇表達式(字符串),或者是有代入值的表達式。
assert_select(element, selector, [equality], [message])
測試選擇器選中的的元素和 _element_
(Nokogiri::XML::Node
的實體或是Nokogiri::XML::NodeSet
) 及其他們的子代是否符合 equality
指定的條件。
例如,可以使用下面的斷言檢測 title 元素的內容:
assert_select "title", "Welcome to Rails Testing Guide"
你也可以用嵌套 assert_select
區塊,為了達到更深入的調查
在下面的範例,內層的 assert_select
區塊,會在外層區塊選中的元素集合中,查詢 li.menu_item
這個元素。
assert_select "ul.navigation" do
assert_select "li.menu_item"
end
除此之外,還可以遍歷外層 assert_select
選中的元素集合,這樣就可以在集合的每個元素上運行內層 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 其他畫面(View)的斷言
這邊有幾個很常用在畫面(View)測試的斷言:
Assertion | Purpose |
---|---|
assert_select_email |
檢查電子信件的內文。 |
assert_select_encoded |
檢查編碼後的HTML。 先解碼各元素的內容,然後在代碼區塊中處理解碼後的各個元素。 |
css_select(selector) or 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、測試小幫手 Testing Helpers
小幫手是一個簡單的模塊,裡面會有一些方法,這些方法你可以在畫面(view)中使用。
為了測試小幫手,你需要做的就是確認小幫手裡面的方法,有沒有跟你預期的一樣,與小助手相關的測試在 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
。
12、測試你的信件 Testing Your Mailers
測試信件類別需要一些特別的工具來做這件事情
12.1 檢查信件運作正常
你的信件類別:和其他Rails的應用程式一樣,應該要被測試以確保他們能如預期的工作。
測試信件類別的目標是確保:
- 信件是可以運作的 (新增、寄送)
- 信件內容是正確的 (主題、寄信人、內容…等等)
- 確保信件是在正確的時間點被寄出去
12.1.1 全面測試
有兩個方向去測試信件,單元測試和功能測試,在單元測試,單獨執行寄送信件,嚴格的控制輸入,並且和已知值(夾具feature)做比較。在功能測試,不用測的這麼仔細,只要確保 controller
、 model
有正確的運作使用信件流程,並在正確的時間點寄出信件。
12.2 單元測試
為了測試你的信件有如預期般的運作,你可以使用單元測試,把信件流程真正得到的結果和預先寫好的值進行比較。
12.2.1 夾具(feature)的另外功用
在單元測試中,夾具(feature)用於設定期望得到的值。因為這些夾具(feature)是範例信件,不是 Active Record
數據,所以要和其他夾具(feature)分開,放在單獨的子目錄中。這個子目錄位於 test/fixtures
資料夾中,其名稱與信件程序對應。例如,信件 UserMailer
使用的夾具(feature)存在 test/fixtures/user_mailer
資料夾中。
生成信件流程時,生成器會為其中每個動作生成相應的stub fixtures
。如果沒使用生成器,要手動創建這些文件。
12.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("me@example.com",
"friend@example.com", Time.now)
# Send the email, then test that it got queued
assert_emails 1 do
email.deliver_now
end
# Test the body of the sent email contains what we expect it to
assert_equal ["me@example.com"], email.from
assert_equal ["friend@example.com"], email.to
assert_equal "You have been invited by me@example.com", email.subject
assert_equal read_fixture("invite").join, email.body.to_s
end
end
在這個測試,我們新增一個信件、並把返回物件賦值給email變數,首先,我們確保信件已經發送了,再來確認信件包含預期的內容, read_fixture
這個輔助方法的作用是從指定的文件中讀取內容。
標註: email.body.to_s
只會在HTML和文本兩者其中之一存在的時候存在,如果 mailer
兩個都有提供,你可以使用 email.text_part.body.to_s
或 email.html_part.body.to_s
,來針對特定的部分測試夾具(fixture)。
這裡是 invite
夾具(fixture)的內容:
Hi friend@example.com,
You have been invited.
Cheers!
現在我們來更深入的了解你的信件測試,在 config/environments/test.rb
文件中,有這麼一行設置 :ActionMailer::Base.delivery_method = :test
。這行設定把發送信件的方法設為 :test
,所以信件並不會真的發送出去(避免測試時騷擾用戶),而是添加到一個陣列中(ActionMailer::Base.deliveries
)。
備註: ActionMailer::Base.deliveries
陣列只會在 ActionMailer::TestCase
和 ActionDispatch::IntegrationTest
測試中自動重新設定,如果你想要在測試之外有乾淨的陣列,你可以用手動重設定 ActionMailer::Base.deliveries.clear
。
12.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("me@example.com", "friend@example.com")
# Test that the email got enqueued with the correct arguments
assert_enqueued_email_with UserMailer, :create_invite, args: ["me@example.com", "friend@example.com"] do
email.deliver_later
end
end
end
這個範例會斷言,透過將參數的 hash
作為 args
傳遞,信件已使用叫做 arguments
的正確信件方法排隊:
require "test_helper"
class UserMailerTest < ActionMailer::TestCase
test "invite" do
# Create the email and store it for further assertions
email = UserMailer.create_invite(from: "me@example.com", to: "friend@example.com")
# Test that the email got enqueued with the correct named arguments
assert_enqueued_email_with UserMailer, :create_invite, args: [{ from: "me@example.com",
to: "friend@example.com" }] do
email.deliver_later
end
end
end
此範例將會斷言,參數化的信件已使用正確的參數和引數進行排隊,信件參數用 params
做傳遞,並且信件方法的引數是 args
:
require "test_helper"
class UserMailerTest < ActionMailer::TestCase
test "invite" do
# Create the email and store it for further assertions
email = UserMailer.with(all: "good").create_invite("me@example.com", "friend@example.com")
# Test that the email got enqueued with the correct mailer parameters and arguments
assert_enqueued_email_with UserMailer, :create_invite, params: { all: "good" },
args: ["me@example.com", "friend@example.com"] do
email.deliver_later
end
end
end
這個範例展示了另外一個測試參數化信件,是否有使用正確的參數做排隊的方法:
require "test_helper"
class UserMailerTest < ActionMailer::TestCase
test "invite" do
# Create the email and store it for further assertions
email = UserMailer.with(to: "friend@example.com").create_invite
# Test that the email got enqueued with the correct mailer parameters
assert_enqueued_email_with UserMailer.with(to: "friend@example.com"), :create_invite do
email.deliver_later
end
end
end
12.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: "friend@example.com" }
end
end
end
# System Test
require "test_helper"
class UsersTest < ActionDispatch::SystemTestCase
driven_by :selenium, using: :headless_chrome
test "inviting a friend" do
visit invite_users_url
fill_in "Email", with: "friend@example.com"
assert_emails 1 do
click_on "Invite"
end
end
end
備註: assert_emails
方法不依賴特定的傳遞方法,並且適用於使用 deliver_now
、 deliver_later
方法傳遞的信件。如果我們明確的想要斷言信件已經排好隊,可以使用 assert_enqueued_email_with
(examples above) 或者 assert_enqueued_emails
方法, 更多資訊可以在這邊找到 documentation here。
13、工作測試 Testing Jobs
因為自定義的工作在應用的不同層排隊,所以我們既要測試工作本身(入隊後的行為),也要測試是否正確入隊了。
13.1 基本的測試用例
預設情況下,當你生成一個工作,一個相對應的測試會自動生成在 test/jobs
資料夾,下面是付款工作的測試範例:
require "test_helper"
class BillingJobTest < ActiveJob::TestCase
test "that account is charged" do
BillingJob.perform_now(account, product)
assert account.reload.charged_for?(product)
end
end
這個測試非常簡單,並且只要斷言這個工作會如預期般的結束
預設下, ActiveJob::TestCase
將會把排隊調整器設定為 :test
,因此你的工作會排隊進行,他還會確保在任何測試執行之前,先清除之前執行和排隊的工作,因此我們可以放心的假設當前測試範圍內沒有執行任何工作。
13.2 在其他組建內客製斷言和測試作業
Active Job
自帶了很多自定義的斷言,可以簡化測試。可用的斷言列表參見ActiveJob::TestHelper
。
不管工作是在哪裡調用的(例如在controller中),最好都要測試工作能正確入隊或執行。這時就體現了 Active Job
提供的自定義斷言的用處。例如,在model中:
require "test_helper"
class ProductTest < ActiveSupport::TestCase
include ActiveJob::TestHelper
test "billing job scheduling" do
assert_enqueued_with(job: BillingJob) do
product.charge(account)
end
end
end
14、Testing Action Cable
因為在你的應用程式裡, Action Cable
被使用於不同的層級,所以你需要同時測試通道(channels)、連結類別(connection classes),而且要確認其他實體(entities)是否廣播(broadcast)正確的消息。
14.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
看這個API文件得到更多資訊 ActionCable::Connection::TestCase
。
14.2 通道測試用例
預設下,當你產生一個通道(channel),還會在 test/channels
資料夾裡面,生成一個相關的測試,這邊有一個聊天頻道的測試範例:
require "test_helper"
class ChatChannelTest < ActionCable::Channel::TestCase
test "subscribes and stream for room" do
# Simulate a subscription creation by calling `subscribe`
subscribe room: "15"
# You can access the Channel object via `subscription` in tests
assert subscription.confirmed?
assert_has_stream "chat_15"
end
end
這個測試非常簡單,他只有斷言此頻道訂閱特定串流的連結。
你還可以指定底層連結標示符,這裡有一個網站通知頻道的測試範例:
require "test_helper"
class WebNotificationsChannelTest < ActionCable::Channel::TestCase
test "subscribes and stream for user" do
stub_connection current_user: users(:john)
subscribe
assert_has_stream_for users(:john)
end
end
看這個API文件得到更多的資訊 ActionCable::Channel::TestCase
。
14.3 其他組建內部的客製化斷言和測試廣播
Action Cable
自帶了一堆字定義的斷言,可用於減少測試的冗長情況,這邊有完整可用的斷言列表,看這個API文件 ActionCable::TestHelper
。
確保在其他組建內(e.g. 你的 controllers
裡面)有廣播正確的訊息,是一個好作法,這就是 Action Cable
自定義斷言非常好用的地方,例如在model中:
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
15、測試預先加載 Testing Eager Loading
通常,應用程式不在急於在 開發 development
、 測試 test
環境中加載,以加快速度,但我們會在 生產 production
環境的時候來做加載。
如果專案中某些文件因為某些原因不能被加載,你最好在部署到 生產 production
環境前檢測到他,對嗎?
15.1 持續整合 CI
如果你的專案有 持續整合 CI
,在 持續整合 CI
中的預先加載,就是一個簡單的方式去確保應用程式有預先加載。
持續整合 CI
通常會設定一些環境變數,用來指示測試套件應該在哪裡運行,例如下面這樣,環境變數設定為 CI
:
# config/environments/test.rb
config.eager_load = ENV["CI"].present?
從Rails 7開始,新生成的應用程式以這種方式設定。
15.2 Bare Test Suites
如果你的專案沒有 持續整合 CI
,你仍然可以在測試插件中,呼叫 Rails.application.eager_load!
來達成預先加載。
15.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
15.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
16、其他測試資源 Additional Testing Resources
16.1 測試與時間有關的程式碼
Rails內建有提供一些小幫手方法,可以讓你斷言你設定的時間有如預期。
這裡有一個範例是使用 travel_to
小幫手:
# Lets say that 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
assert_equal Date.new(2004, 10, 24), user.activation_date # inside the `travel_to` block `Date.current` is mocked
assert user.applicable_for_gifting?
end
assert_equal Date.new(2004, 10, 24), user.activation_date # The change was visible only inside the `travel_to` block.
可以看這個時間小幫手的API文件,來得到更多的資訊 ActiveSupport::Testing::TimeHelpers
API Documentation。