Active Record Fixtures (測試資料)
Fixtures 是一種組織測試資料的方式;簡而言之,就是樣本資料。
它們儲存在 YAML 檔案中,每個模型一個檔案,預設放置在 <您的 Rails 應用程式>/test/fixtures/
或任何應用程式引擎下的 test/fixtures
資料夾中。
在 test_helper.rb
中加入 require "rails/test_help"
後,也可以使用 ActiveSupport::TestCase.fixture_paths=
來更改位置。
Fixture 檔案以 .yml
副檔名結尾,例如:<您的 Rails 應用程式>/test/fixtures/web_sites.yml
)。
Fixture 檔案的格式如下所示
rubyonrails:
id: 1
name: Ruby on Rails
url: http://www.rubyonrails.org
google:
id: 2
name: Google
url: http://www.google.com
這個 Fixture 檔案包含兩個 Fixture。每個 YAML Fixture(即記錄)都被賦予一個名稱,後面跟著一個縮排的鍵值對列表,格式為「鍵:值」。為了方便閱讀,記錄之間用空行分隔。
排序
Fixtures 預設是無序的。這是因為 YAML 中的映射是無序的。
如果您需要有序的 Fixtures,請使用 omap YAML 類型。規格請參閱 yaml.org/type/omap.html。
當您在同一個表格的鍵上設定外部鍵約束時,您將需要有序的 Fixture。這通常用於樹狀結構。
例如
--- !omap
- parent:
id: 1
parent_id: NULL
title: Parent
- child:
id: 2
parent_id: 1
title: Child
在測試案例中使用 Fixtures
由於 Fixtures 是一種測試結構,我們在單元和功能測試中使用它們。有兩種方法可以使用 Fixtures,但首先讓我們看一下單元測試範例
require "test_helper"
class WebSiteTest < ActiveSupport::TestCase
test "web_site_count" do
assert_equal 2, WebSite.count
end
end
預設情況下,test_helper.rb
會將所有 Fixture 載入到測試資料庫中,因此此測試將會成功。
測試環境會在每次測試之前自動將所有 Fixture 載入到資料庫中。為了確保資料一致性,環境會在執行載入之前刪除 Fixtures。
除了可在資料庫中使用之外,Fixture 的資料也可以使用與模型同名的特殊動態方法來存取。
將 Fixture 名稱傳遞給此動態方法會返回符合此名稱的 Fixture
test "find one" do
assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
end
傳遞多個 Fixture 名稱會返回符合這些名稱的所有 Fixture
test "find all by name" do
assert_equal 2, web_sites(:rubyonrails, :google).length
end
不傳遞任何參數會返回所有 Fixture
test "find all" do
assert_equal 2, web_sites.length
end
傳遞任何不存在的 Fixture 名稱將會引發 StandardError
test "find by name that does not exist" do
assert_raise(StandardError) { web_sites(:reddit) }
end
如果模型名稱與 TestCase
方法衝突,您可以使用通用的 fixture
存取器
test "generic find" do
assert_equal "Ruby on Rails", fixture(:web_sites, :rubyonrails).name
end
或者,您可以啟用 Fixture 資料的自動實例化。例如,以下測試
test "find_alt_method_1" do
assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
end
test "find_alt_method_2" do
assert_equal "Ruby on Rails", @rubyonrails.name
end
為了在測試案例中使用這些方法來存取 Fixture 資料,您必須在 ActiveSupport::TestCase
衍生的類別中指定以下其中一項
-
完全啟用實例化 Fixture(啟用上述的替代方法 #1 和 #2)
self.use_instantiated_fixtures = true
-
僅建立 Fixture 的雜湊,不要「尋找」每個實例(僅啟用替代方法 #1)
self.use_instantiated_fixtures = :no_instances
使用這兩種替代方法中的任何一種都會導致效能下降,因為必須完整遍歷資料庫中的 Fixture 資料才能建立 Fixture 雜湊和/或實例變數。對於大型 Fixture 資料集來說,這是很耗費資源的。
使用 ERB 的動態 Fixture
有時您不太關心 Fixture 的內容,而更關心數量。在這些情況下,您可以將 ERB
與 YAML Fixture 混合使用來建立一堆用於負載測試的 Fixture,例如
<% 1.upto(1000) do |i| %>
fix_<%= i %>:
id: <%= i %>
name: guy_<%= i %>
<% end %>
這將建立 1000 個非常簡單的 Fixture。
使用 ERB
,您也可以使用插入語法(例如 <%= Date.today.strftime("%Y-%m-%d") %>
)將動態值注入 Fixture 中。然而,這是一個需要謹慎使用的功能。Fixture 的重點是它們是可預測的穩定樣本資料單元。如果您覺得需要注入動態值,那麼也許您應該重新檢查您的應用程式是否可以正確測試。因此,Fixture 中的動態值應被視為程式碼異味。
在 Fixture 中定義的輔助方法將無法在其他 Fixture 中使用,以防止不必要的測試間依賴關係。多個 Fixture 使用的方法應在包含在 ActiveRecord::FixtureSet.context_class
的模組中定義。
-
在
test_helper.rb
中定義輔助方法module FixtureFileHelpers def file_sha(path) OpenSSL::Digest::SHA256.hexdigest(File.read(Rails.root.join('test/fixtures', path))) end end ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers
-
在 Fixture 中使用輔助方法
photo: name: kitten.png sha: <%= file_sha 'files/kitten.png' %>
交易測試
測試案例可以使用 begin+rollback 來隔離它們對資料庫的更改,而不必為每個測試案例執行刪除+插入操作。
class FooTest < ActiveSupport::TestCase
self.use_transactional_tests = true
test "godzilla" do
assert_not_empty Foo.all
Foo.destroy_all
assert_empty Foo.all
end
test "godzilla aftermath" do
assert_not_empty Foo.all
end
end
如果您使用所有 Fixture 資料預載測試資料庫(可能是透過執行 bin/rails db:fixtures:load
),並使用交易測試,那麼您可以省略測試案例中的所有 Fixture 宣告,因為所有資料都已存在,且每個案例都會回滾其更改。
要將實例化 Fixture 與預載資料一起使用,請將 self.pre_loaded_fixtures
設定為 true。這將提供對每個已透過 Fixture 載入的表格的 Fixture 資料的存取權(取決於 use_instantiated_fixtures
的值)。
不使用交易測試的時機
-
您正在測試交易是否正常運作。巢狀交易在所有父交易提交之前都不會提交,尤其是 Fixture 交易,它在設定中開始,並在拆解中回滾。因此,在 Active Record 支援巢狀交易或儲存點(正在進行中)之前,您將無法驗證交易的結果。
-
您的資料庫不支援交易。除了 MySQL MyISAM 之外,每個 Active Record 資料庫都支援交易。請改用 InnoDB、MaxDB 或 NDB。
進階 Fixture
未指定 ID 的 Fixture 具有一些額外功能
-
穩定、自動產生的 ID
-
關聯的標籤參考 (belongs_to、has_one、has_many)
-
HABTM 關聯作為內嵌列表
即使指定了 ID,也有一些更進階的功能可用
-
自動填入時間戳記欄位
-
Fixture 標籤插值
-
支援 YAML 預設值
穩定、自動產生的 ID
這裡,有一個猴子 Fixture
george:
id: 1
name: George the Monkey
reginald:
id: 2
name: Reginald the Pirate
這些 Fixture 中的每一個都有兩個唯一的識別符:一個用於資料庫,一個用於人類。為什麼我們不產生主鍵呢?雜湊每個 Fixture 的標籤會產生一致的 ID
george: # generated id: 503576764
name: George the Monkey
reginald: # generated id: 324201669
name: Reginald the Pirate
Active Record 會查看 Fixture 的模型類別,找出正確的主鍵,並在將 Fixture 插入資料庫之前產生它。
給定標籤的產生的 ID 是常數,因此只要我們知道標籤,就可以在不載入任何內容的情況下找出任何 Fixture 的 ID。
關聯的標籤參考 (belongs_to
、has_one
、has_many
)
在 Fixture 中指定外部鍵可能非常脆弱,更不用說難以閱讀了。由於 Active Record 可以從其標籤中找出任何 Fixture 的 ID,因此您可以使用標籤而不是 ID 來指定 FK。
belongs_to
讓我們再找一些猴子和海盜。
### in pirates.yml
reginald:
id: 1
name: Reginald the Pirate
monkey_id: 1
### in monkeys.yml
george:
id: 1
name: George the Monkey
pirate_id: 1
新增更多猴子和海盜,並將其分解成多個檔案,就會變得難以追蹤發生的事情。讓我們使用標籤代替 ID
### in pirates.yml
reginald:
name: Reginald the Pirate
monkey: george
### in monkeys.yml
george:
name: George the Monkey
pirate: reginald
砰!一切都清楚了。Active Record 會反映 Fixture 的模型類別,找到所有 belongs_to
關聯,並允許您為關聯指定目標標籤(猴子:george),而不是為FK指定目標id(monkey_id: 1
)。
多型 belongs_to
支援多型關係稍微複雜一些,因為 Active Record 需要知道您的關聯指向的類型。像這樣的東西應該很熟悉
### in fruit.rb
belongs_to :eater, polymorphic: true
### in fruits.yml
apple:
id: 1
name: apple
eater_id: 1
eater_type: Monkey
我們可以做得更好嗎?當然可以!
apple:
eater: george (Monkey)
只需提供多型目標類型,Active Record 就會處理剩下的事情。
has_and_belongs_to_many
或 has_many :through
是時候給我們的猴子一些水果了。
### in monkeys.yml
george:
id: 1
name: George the Monkey
### in fruits.yml
apple:
id: 1
name: apple
orange:
id: 2
name: orange
grape:
id: 3
name: grape
### in fruits_monkeys.yml
apple_george:
fruit_id: 1
monkey_id: 1
orange_george:
fruit_id: 2
monkey_id: 1
grape_george:
fruit_id: 3
monkey_id: 1
讓我們移除 HABTM Fixture。
### in monkeys.yml
george:
id: 1
name: George the Monkey
fruits: apple, orange, grape
### in fruits.yml
apple:
name: apple
orange:
name: orange
grape:
name: grape
咻!不再有 fruits_monkeys.yml 檔案。我們在 George 的 Fixture 上指定了水果列表,但我們也可以輕鬆地在每個水果上指定猴子列表。與 belongs_to
一樣,Active Record 會反映 Fixture 的模型類別並找出 has_and_belongs_to_many
關聯。
自動填入時間戳記欄位
如果您的表格/模型指定了任何 Active Record 的標準時間戳記欄位(created_at
、created_on
、updated_at
、updated_on
),它們將會自動設定為 Time.now
。
如果您已設定特定值,它們將會保留不變。
Fixture 標籤插值
目前 Fixture 的標籤始終可作為欄位值使用
geeksomnia:
name: Geeksomnia's Account
subdomain: $LABEL
email: $LABEL@email.com
此外,有時(例如在移植較舊的連接表格 Fixture 時)您需要能夠取得給定標籤的識別符。 ERB
來救援
george_reginald:
monkey_id: <%= ActiveRecord::FixtureSet.identify(:reginald) %>
pirate_id: <%= ActiveRecord::FixtureSet.identify(:george) %>
如果模型使用 UUID 值作為識別符,請新增 :uuid
參數
ActiveRecord::FixtureSet.identify(:boaty_mcboatface, :uuid)
支援 YAML 預設值
您可以在 Fixture YAML 檔案中設定和重複使用預設值。這與在 database.yml
檔案中用於指定預設值的技術相同
DEFAULTS: &DEFAULTS
created_on: <%= 3.weeks.ago.to_fs(:db) %>
first:
name: Smurf
<<: *DEFAULTS
second:
name: Fraggle
<<: *DEFAULTS
任何標記為「DEFAULTS」的 Fixture 都會被安全地忽略。
除了使用「DEFAULTS」之外,您還可以透過在「_fixture」區段中設定「ignore」來指定要忽略的 Fixture。
# users.yml
_fixture:
ignore:
- base
# or use "ignore: base" when there is only one fixture that needs to be ignored.
base: &base
admin: false
introduction: "This is a default description"
admin:
<<: *base
admin: true
visitor:
<<: *base
在上面的範例中,在建立 Fixture 時會忽略「base」。這可以用於繼承通用屬性。
複合主鍵 Fixture
複合主鍵表格的 Fixture 與普通表格非常相似。使用 id 欄位時,可以照常省略該欄位
# app/models/book.rb
class Book < ApplicationRecord
self.primary_key = [:author_id, :id]
belongs_to :author
end
# books.yml
alices_adventure_in_wonderland:
author_id: <%= ActiveRecord::FixtureSet.identify(:lewis_carroll) %>
title: "Alice's Adventures in Wonderland"
但是,為了支援複合主鍵關係,您必須使用「composite_identify」方法
# app/models/book_orders.rb
class BookOrder < ApplicationRecord
self.primary_key = [:shop_id, :id]
belongs_to :order, foreign_key: [:shop_id, :order_id]
belongs_to :book, foreign_key: [:author_id, :book_id]
end
# book_orders.yml
alices_adventure_in_wonderland_in_books:
author: lewis_carroll
book_id: <%= ActiveRecord::FixtureSet.composite_identify(
:alices_adventure_in_wonderland, Book.primary_key)[:id] %>
shop: book_store
order_id: <%= ActiveRecord::FixtureSet.composite_identify(
:books, Order.primary_key)[:id] %>
設定 Fixture 模型類別
可以直接在 YAML 檔案中設定 Fixture 的模型類別。當 Fixture 在測試之外載入且 set_fixture_class
不可用時(例如,在執行 bin/rails db:fixtures:load
時),這會很有幫助。
_fixture:
model_class: User
david:
name: David
任何標記為「_fixture」的 Fixture 都會被安全地忽略。
- #
- C
- E
- F
- I
- N
- R
- S
- T
常數
最大 ID | = | 2**30 - 1 |
屬性
[R] | 設定 | |
[R] | Fixture | |
[R] | 被忽略的 Fixture | |
[R] | 模型類別 | |
[R] | 名稱 | |
[R] | table_name(表格名稱) |
類別公開方法(Class Public methods)
cache_fixtures(connection_pool, fixtures_map) 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 576 def cache_fixtures(connection_pool, fixtures_map) cache_for_connection_pool(connection_pool).update(fixtures_map) end
cache_for_connection_pool(connection_pool) 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 560 def cache_for_connection_pool(connection_pool) @@all_cached_fixtures[connection_pool] end
cached_fixtures(connection_pool, keys_to_fetch = nil) 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 568 def cached_fixtures(connection_pool, keys_to_fetch = nil) if keys_to_fetch cache_for_connection_pool(connection_pool).values_at(*keys_to_fetch) else cache_for_connection_pool(connection_pool).values end end
composite_identify(label, key) 連結
傳回一個一致的、平台獨立的雜湊,表示標籤 (label) 與提供的複合鍵 (composite key) 子組件之間的映射。
範例(Example)
composite_identify("label", [:a, :b, :c]) # => { a: hash_1, b: hash_2, c: hash_3 }
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 633 def composite_identify(label, key) key .index_with .with_index { |sub_key, index| (identify(label) << index) % MAX_ID } .with_indifferent_access end
context_class() 連結
ERB Fixtures 使用的評估上下文之父類別。
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 641 def context_class @context_class ||= Class.new end
create_fixtures(fixtures_directories, fixture_set_names, class_names = {}, config = ActiveRecord::Base) 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 595 def create_fixtures(fixtures_directories, fixture_set_names, class_names = {}, config = ActiveRecord::Base) fixture_set_names = Array(fixture_set_names).map(&:to_s) class_names.stringify_keys! connection_pool = config.connection_pool fixture_files_to_read = fixture_set_names.reject do |fs_name| fixture_is_cached?(connection_pool, fs_name) end if fixture_files_to_read.any? fixtures_map = read_and_insert( Array(fixtures_directories), fixture_files_to_read, class_names, connection_pool, ) cache_fixtures(connection_pool, fixtures_map) end cached_fixtures(connection_pool, fixture_set_names) end
fixture_is_cached?(connection_pool, table_name) 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 564 def fixture_is_cached?(connection_pool, table_name) cache_for_connection_pool(connection_pool)[table_name] end
identify(label, column_type = :integer) 連結
傳回 label
一致的、平台獨立的識別碼。
整數識別碼的值小於 2^30。UUID 是 RFC 4122 版本 5 的 SHA-1 雜湊值。
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 619 def identify(label, column_type = :integer) if column_type == :uuid Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s) else Zlib.crc32(label.to_s) % MAX_ID end end
instantiate_all_loaded_fixtures(object, load_instances = true) 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 589 def instantiate_all_loaded_fixtures(object, load_instances = true) all_loaded_fixtures.each_value do |fixture_set| instantiate_fixtures(object, fixture_set, load_instances) end end
instantiate_fixtures(object, fixture_set, load_instances = true) 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 580 def instantiate_fixtures(object, fixture_set, load_instances = true) return unless load_instances fixture_set.each do |fixture_name, fixture| object.instance_variable_set "@#{fixture_name}", fixture.find rescue FixtureClassNotFound nil end end
new(_, name, class_name, path, config = ActiveRecord::Base) 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 713 def initialize(_, name, class_name, path, config = ActiveRecord::Base) @name = name @path = path @config = config self.model_class = class_name @fixtures = read_fixture_files(path) @table_name = model_class&.table_name || self.class.default_fixture_table_name(name, config) end
reset_cache() 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 556 def reset_cache @@all_cached_fixtures.clear end
實例公開方法(Instance Public methods)
[](x) 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 724 def [](x) fixtures[x] end
[]=(k, v) 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 728 def []=(k, v) fixtures[k] = v end
each(&block) 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 732 def each(&block) fixtures.each(&block) end
size() 連結
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 736 def size fixtures.size end
table_rows() 連結
傳回一個要插入的資料列雜湊。鍵是表格,值是要插入到該表格的資料列清單。
程式碼: 顯示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 742 def table_rows # allow specifying fixtures to be ignored by setting `ignore` in `_fixture` section fixtures.except!(*ignored_fixtures) TableRows.new( table_name, model_class: model_class, fixtures: fixtures, ).to_hash end