Active Record Fixtures
Fixtures 是一种组织测试数据的方案,简而言之,就是示例数据。
它们存储在 YAML 文件中,每个模型一个文件,默认情况下放置在 <your-rails-app>/test/fixtures/
或任何应用程序引擎下的 test/fixtures
文件夹中。
在 test_helper.rb
中添加 require "rails/test_help"
后,也可以使用 ActiveSupport::TestCase.fixture_paths=
更改位置。
Fixture 文件以 .yml
文件扩展名结尾,例如:<your-rails-app>/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(即记录)都给定一个名称,后面跟着一个缩进的键值对列表,格式为“键:值”。为了便于查看,记录之间用空行隔开。
排序
默认情况下,Fixture 是无序的。这是因为 YAML 中的映射是无序的。
如果需要有序 Fixture,请使用 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
在测试用例中使用 Fixture
由于 Fixture 是测试结构,因此我们在单元测试和功能测试中使用它们。使用 Fixture 有两种方法,但首先让我们看一下单元测试示例
require "test_helper"
class WebSiteTest < ActiveSupport::TestCase
test "web_site_count" do
assert_equal 2, WebSite.count
end
end
默认情况下,test_helper.rb
会将所有 fixtures 加载到您的测试数据库中,因此此测试将成功。
测试环境会在每次测试之前自动将所有 fixtures 加载到数据库中。为了确保数据一致性,环境会在运行加载之前删除 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
或者,您可以启用 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
派生类中指定以下内容之一
-
完全启用实例化的 fixtures(启用上面的备用方法 #1 和 #2)
self.use_instantiated_fixtures = true
-
仅创建 fixtures 的哈希,不要“查找”每个实例(仅启用备用方法 #1)
self.use_instantiated_fixtures = :no_instances
使用这两种备用方法中的任何一种都会导致性能下降,因为必须在数据库中完全遍历 fixture 数据才能创建 fixture 哈希和/或实例变量。对于大型的 fixture 数据集来说,这是很昂贵的。
使用 ERB 的动态 fixtures
有时您并不关心 fixtures 的内容,而是关心它们的体积。在这种情况下,您可以将 ERB
与您的 YAML fixtures 混合使用,以创建大量用于负载测试的 fixtures,例如
<% 1.upto(1000) do |i| %>
fix_<%= i %>:
id: <%= i %>
name: guy_<%= i %>
<% end %>
这将创建 1000 个非常简单的 fixtures。
使用 ERB
,您还可以使用诸如 <%= Date.today.strftime("%Y-%m-%d") %>
之类的插入将动态值注入到您的 fixtures 中。但是,这是一种需要谨慎使用的功能。fixtures 的要点是它们是可预测的样本数据的稳定单元。如果您觉得需要注入动态值,那么您可能应该重新检查您的应用程序是否可测试。因此,fixtures 中的动态值应该被视为代码异味。
在 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
如果您使用所有固定数据预加载测试数据库(可能通过运行 bin/rails db:fixtures:load
)并使用事务测试,那么您可以在测试用例中省略所有固定数据声明,因为所有数据都已存在,并且每个用例都会回滚其更改。
为了使用预加载数据的实例化固定数据,将 self.pre_loaded_fixtures
设置为 true。这将为通过固定数据加载的每个表提供对固定数据访问权限(取决于 use_instantiated_fixtures
的值)。
何时**不**使用事务测试
-
您正在测试事务是否正常工作。嵌套事务在所有父事务提交之前不会提交,特别是 setup 中开始并在 teardown 中回滚的固定数据事务。因此,您将无法验证事务的结果,直到 Active Record 支持嵌套事务或保存点(正在进行中)。
-
您的数据库不支持事务。除了 MySQL MyISAM 之外,每个 Active Record 数据库都支持事务。请改用 InnoDB、MaxDB 或 NDB。
高级固定数据
未指定 ID 的固定数据具有一些额外的功能
-
稳定、自动生成的 ID
-
关联的标签引用(belongs_to、has_one、has_many)
-
HABTM 关联作为内联列表
即使指定了 id,也有一些更高级的功能可用
-
自动填充时间戳列
-
固定数据标签插值
-
支持 YAML 默认值
稳定、自动生成的 ID
这里有一个猴子固定数据
george:
id: 1
name: George the Monkey
reginald:
id: 2
name: Reginald the Pirate
这些固定数据中的每一个都有两个唯一的标识符:一个用于数据库,一个用于人类。为什么我们不生成主键呢?对每个固定数据的标签进行哈希处理会产生一个一致的 ID
george: # generated id: 503576764
name: George the Monkey
reginald: # generated id: 324201669
name: Reginald the Pirate
Active Record 查看固定数据的模型类,发现正确的 primary key,并在将固定数据插入数据库之前生成它。
给定标签的生成 ID 是恒定的,因此只要知道标签,我们就可以在不加载任何内容的情况下发现任何固定数据的 ID。
关联的标签引用(belongs_to
、has_one
、has_many
)
在固定数据中指定外键可能非常脆弱,更不用说难以阅读了。由于 Active Record 可以从标签中找出任何固定数据的 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 反映在固定装置的模型类上,找到所有 belongs_to
关联,并允许您为 **关联** 指定目标 **标签**(猴子:乔治),而不是为 **外键** 指定目标 **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 固定装置消失。
### 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 文件。我们在乔治的固定装置上指定了水果列表,但我们也可以在每种水果上指定猴子列表。与 belongs_to
一样,Active Record 反映在固定装置的模型类上,并发现 has_and_belongs_to_many
关联。
自动填充时间戳列
如果您的表/模型指定了任何 Active Record 的标准时间戳列(created_at
、created_on
、updated_at
、updated_on
),它们将自动设置为 Time.now
。
如果您设置了特定值,它们将保持不变。
固定数据标签插值
当前固定装置的标签始终可用作列值
geeksomnia:
name: Geeksomnia's Account
subdomain: $LABEL
email: $LABEL@email.com
此外,有时(例如在移植旧的联接表固定装置时)您需要能够获取给定标签的标识符。 ERB
来救援
george_reginald:
monkey_id: <%= ActiveRecord::FixtureSet.identify(:reginald) %>
pirate_id: <%= ActiveRecord::FixtureSet.identify(:george) %>
如果模型对标识符使用 UUID 值,请添加 :uuid
参数
ActiveRecord::FixtureSet.identify(:boaty_mcboatface, :uuid)
支持 YAML 默认值
您可以在固定装置 YAML 文件中设置和重用默认值。这与在 database.yml
文件中用于指定默认值的相同技术。
DEFAULTS: &DEFAULTS
created_on: <%= 3.weeks.ago.to_fs(:db) %>
first:
name: Smurf
<<: *DEFAULTS
second:
name: Fraggle
<<: *DEFAULTS
任何标记为“DEFAULTS”的固定装置都会被安全地忽略。
除了使用“DEFAULTS”之外,您还可以通过在“_fixture”部分中设置“ignore”来指定哪些固定装置将被忽略。
# 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
在上面的示例中,创建固定装置时将忽略“base”。这可用于继承公共属性。
复合主键固定装置
复合主键表的固定装置与普通表非常相似。使用 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, query_constraints: [:shop_id, :order_id]
belongs_to :book, query_constraints: [: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] %>
配置固定装置模型类
可以在 YAML 文件中直接设置夹具的模型类。当夹具在测试之外加载并且 set_fixture_class
不可用时(例如,在运行 bin/rails db:fixtures:load
时),这很有用。
_fixture:
model_class: User
david:
name: David
任何标记为“_fixture”的夹具都会被安全地忽略。
- #
- C
- E
- F
- I
- N
- R
- S
- T
常量
MAX_ID | = | 2**30 - 1 |
属性
[R] | config | |
[R] | fixtures | |
[R] | ignored_fixtures | |
[R] | model_class | |
[R] | name | |
[R] | table_name |
类公共方法
cache_fixtures(connection, fixtures_map) 链接
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 572 def cache_fixtures(connection, fixtures_map) cache_for_connection(connection).update(fixtures_map) end
cache_for_connection(connection) 链接
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 556 def cache_for_connection(connection) @@all_cached_fixtures[connection] end
cached_fixtures(connection, keys_to_fetch = nil) 链接
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 564 def cached_fixtures(connection, keys_to_fetch = nil) if keys_to_fetch cache_for_connection(connection).values_at(*keys_to_fetch) else cache_for_connection(connection).values end end
composite_identify(label, key) 链接
返回一个一致的、与平台无关的哈希,表示标签和提供的复合键的子组件之间的映射。
示例
composite_identify("label", [:a, :b, :c]) # => { a: hash_1, b: hash_2, c: hash_3 }
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 631 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 夹具使用的评估上下文的超类。
来源: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 639 def context_class @context_class ||= Class.new end
create_fixtures(fixtures_directories, fixture_set_names, class_names = {}, config = ActiveRecord::Base, &block) 链接
来源: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 591 def create_fixtures(fixtures_directories, fixture_set_names, class_names = {}, config = ActiveRecord::Base, &block) fixture_set_names = Array(fixture_set_names).map(&:to_s) class_names.stringify_keys! # FIXME: Apparently JK uses this. connection = block_given? ? block : lambda { ActiveRecord::Base.connection } fixture_files_to_read = fixture_set_names.reject do |fs_name| fixture_is_cached?(connection.call, fs_name) end if fixture_files_to_read.any? fixtures_map = read_and_insert( Array(fixtures_directories), fixture_files_to_read, class_names, connection, ) cache_fixtures(connection.call, fixtures_map) end cached_fixtures(connection.call, fixture_set_names) end
fixture_is_cached?(connection, table_name) 链接
来源: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 560 def fixture_is_cached?(connection, table_name) cache_for_connection(connection)[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 617 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 585 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 576 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 709 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 552 def reset_cache @@all_cached_fixtures.clear end
实例公共方法
[](x) 链接
来源: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 720 def [](x) fixtures[x] end
[]=(k, v) 链接
来源: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 724 def []=(k, v) fixtures[k] = v end
each(&block) 链接
来源: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 728 def each(&block) fixtures.each(&block) end
size() 链接
来源: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 732 def size fixtures.size end
table_rows() 链接
返回要插入的行哈希。键是表,值是要插入到该表中的行列表。
来源: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/fixtures.rb, line 738 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