Active Record 夹具¶ ↑
夹具是一种组织数据的方法,您想用这些数据进行测试;简而言之,就是示例数据。
它们存储在 YAML 文件中,每个模型一个文件,默认情况下放置在 <your-rails-app>/test/fixtures/ 或应用程序任何引擎下的 test/fixtures 文件夹中。
可以通过 ActiveSupport::TestCase.fixture_paths= 更改位置,前提是您的 test_helper.rb 中已经 require "rails/test_help"。
夹具文件以 .yml 文件扩展名结尾,例如:<your-rails-app>/test/fixtures/web_sites.yml)。
夹具文件的格式如下:
rubyonrails: id: 1 name: Ruby on Rails url: http://www.rubyonrails.org google: id: 2 name: Google url: http://www.google.com
此夹具文件包含两个夹具。每个 YAML 夹具(即记录)都有一个名称,后面跟着一个缩进的键/值对列表,格式为“键: 值”。记录之间用空行分隔,方便查看。
排序¶ ↑
夹具默认是无序的。这是因为 YAML 中的映射是无序的。
如果您想要有序夹具,请使用 omap YAML 类型。有关规范,请参阅 yaml.org/type/omap.html。
当您在同一表中的键上有外键约束时,需要有序夹具。这通常是树状结构所必需的。
例如
--- !omap
- parent:
id: 1
parent_id: NULL
title: Parent
- child:
id: 2
parent_id: 1
title: Child
在测试用例中使用夹具¶ ↑
由于夹具是测试的构造,我们在单元测试和功能测试中使用它们。有两种方法可以使用夹具,但首先让我们看一个示例单元测试。
require "test_helper" class WebSiteTest < ActiveSupport::TestCase test "web_site_count" do assert_equal 2, WebSite.count end end
默认情况下,test_helper.rb 会将所有夹具加载到您的测试数据库中,因此此测试将成功。
测试环境会在每次测试之前自动将所有夹具加载到数据库中。为了确保数据一致性,环境会在运行加载之前删除夹具。
除了可以在数据库中访问外,还可以通过使用一个特殊的动态方法来访问夹具数据,该方法与模型名称相同。
将夹具名称传递给此动态方法将返回匹配此名称的夹具。
test "find one" do assert_equal "Ruby on Rails", web_sites(:rubyonrails).name end
传递多个夹具名称将返回匹配这些名称的所有夹具。
test "find all by name" do assert_equal 2, web_sites(:rubyonrails, :google).length end
不传递参数将返回所有夹具。
test "find all" do assert_equal 2, web_sites.length end
传递任何不存在的夹具名称将引发 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
或者,您可以启用夹具数据的自动实例化。例如,考虑以下测试。
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
为了在测试用例中使用这些方法访问夹具数据,您必须在您的 ActiveSupport::TestCase 派生类中指定以下之一。
-
完全启用实例化夹具(启用上面备用方法 #1 和 #2)。
self.use_instantiated_fixtures = true
-
仅创建夹具的哈希,不要“查找”每个实例(仅启用备用方法 #1)。
self.use_instantiated_fixtures = :no_instances
使用这两种备用方法中的任何一种都会对性能产生影响,因为必须完全遍历数据库中的夹具数据才能创建夹具哈希和/或实例变量。对于大量夹具数据来说,这非常昂贵。
动态夹具与 ERB¶ ↑
有时,您并不太关心夹具的内容,而更关心其数量。在这些情况下,您可以将 ERB 与 YAML 夹具混合使用,以创建大量夹具用于负载测试,例如:
<% 1.upto(1000) do |i| %> fix_<%= i %>: id: <%= i %> name: guy_<%= i %> <% end %>
这将创建 1000 个非常简单的夹具。
使用 ERB,您还可以通过类似 <%= Date.today.strftime("%Y-%m-%d") %> 的插入来注入动态值。但是,这是一个需要谨慎使用的功能。夹具的意义在于它们是稳定、可预测的示例数据单元。如果您觉得需要注入动态值,那么您应该重新审视您的应用程序是否可以正确测试。因此,夹具中的动态值应被视为代码异味。
在夹具中定义的辅助方法将无法在其他夹具中使用,以防止不期望的测试间依赖。多个夹具使用的方法应定义在一个模块中,该模块包含在 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
-
在夹具中使用辅助方法。
photo: name: kitten.png sha: <%= file_sha 'files/kitten.png' %>
事务测试¶ ↑
测试用例可以使用 begin+rollback 来隔离其对数据库的更改,而不是为每个测试用例执行 delete+insert。
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: 380982691 name: George the Monkey reginald: # generated id: 41001176 name: Reginald the Pirate
Active Record 会查看夹具的模型类,发现正确的 主键,并在将夹具插入数据库之前生成它。
给定标签的生成 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 关联,并允许您指定关联的目标标签(monkey: george),而不是外键的目标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
让我们移除 fruits_monkeys.yml 文件。
### 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 的夹具中指定了水果列表,但同样,我们也可以在每个水果上指定猴子列表。与 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, 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] %>
配置夹具模型类¶ ↑
可以直接在 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 |
Attributes
| [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 夹具使用的评估上下文的超类。
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
返回要插入的行的哈希。键是表,值是要插入到该表的行列表。
# 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