Active Record Fixtures
Fixtures 是一种组织测试数据的方案,简单来说,就是示例数据。
它们存储在 YAML 文件中,每个模型一个文件,默认情况下放置在 <your-rails-app>/test/fixtures/
或您应用程序任何引擎下的 test/fixtures
文件夹中。
位置也可以使用 ActiveSupport::TestCase.fixture_paths=
更改,前提是在您的 test_helper.rb
中包含 require "rails/test_help"
。
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(即记录)都有一个名称,后面跟着一个缩进的键值对列表,采用“key: value”格式。为了方便查看,记录之间用空行隔开。
排序
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
会将所有 Fixture 加载到测试数据库中,因此此测试将成功。
测试环境将在每次测试之前自动将所有 Fixture 加载到数据库中。为了确保数据一致性,环境会在运行加载之前删除 Fixture。
除了在数据库中可用外,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 事务,它在 setup 中开始并在 teardown 中回滚。因此,您将无法验证事务的结果,直到 Active Record 支持嵌套事务或保存点(正在进行中)。
-
您的数据库不支持事务。除了 MySQL MyISAM 之外,每个 Active Record 数据库都支持事务。请改用 InnoDB、MaxDB 或 NDB。
高级 Fixture
不指定 ID 的 Fixture 有一些额外的功能
-
稳定的、自动生成的 ID
-
关联(belongs_to、has_one、has_many)的标签引用
-
HABTM 关联作为内联列表
即使指定了 id,也有一些更高级的功能可用
-
自动填充的 timestamp 列
-
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
关联,并允许您为关联(monkey: george)指定一个目标标签,而不是为FK(monkey_id: 1
)指定一个目标id。
多态 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
关联。
自动填充的 Timestamp 列
如果您的表/模型指定了任何 Active Record 的标准 timestamp 列(created_at
、created_on
、updated_at
、updated_on
),它们将自动设置为 Time.now
。
如果您设置了特定值,它们将保持不变。
Fixture 标签插值
当前 Fixture 的标签始终可用作列值
geeksomnia:
name: Geeksomnia's Account
subdomain: $LABEL
email: [email protected]
此外,有时(例如,在移植旧的联接表 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
常量
MAX_ID | = | 2**30 - 1 |
属性
[R] | config | |
[R] | fixtures | |
[R] | ignored_fixtures | |
[R] | model_class | |
[R] | name | |
[R] | table_name |
类公共方法
cache_fixtures(connection_pool, fixtures_map) Link
cache_for_connection_pool(connection_pool) Link
cached_fixtures(connection_pool, keys_to_fetch = nil) Link
composite_identify(label, key) Link
返回一个一致的、与平台无关的哈希,表示标签和提供的复合键的子组件之间的映射。
示例
composite_identify("label", [:a, :b, :c]) # => { a: hash_1, b: hash_2, c: hash_3 }
context_class() Link
ERB fixture 使用的评估上下文的超类。
create_fixtures(fixtures_directories, fixture_set_names, class_names = {}, config = ActiveRecord::Base) Link
# 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) Link
identify(label, column_type = :integer) Link
返回 `label` 的一致且与平台无关的标识符。
整数标识符是小于 2^30 的值。 UUID 是 RFC 4122 版本 5 SHA-1 哈希。
instantiate_all_loaded_fixtures(object, load_instances = true) Link
instantiate_fixtures(object, fixture_set, load_instances = true) Link
# 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) Link
# 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() Link
实例公共方法
[](x) Link
[]=(k, v) Link
each(&block) Link
size() Link
table_rows() Link
返回要插入的行哈希。键是表,值是要插入该表的行列表。