Test Fixtures
跟踪一下fixtures是做什么的
class ProductTest < ActiveSupport::TestCase
require 'trace_tree'
binding.trace_tree(html: true, tmp: ['rails', 'fixtures.html']) do
fixtures :products
end
test "product attributes must not be empty" do
product = Product.new
assert product.invalid?
assert product.errors[:title].any?
end
end
完整调用栈如下
本来以为此方法定义在ActiveSupport::TestCase之中,却发现竟然来自于active_record,猜想应该是active_record本来做测试时也会用到,而ActiveSupport::TestCase 就顺便借用下
值得关注的是,下面无端端调用了一堆Class#new、Module#initialize和include,回忆起来,这种pattern在rails中还是挺常用的,用途就是创建匿名module然后动态地mixin,controller中的xx_url、xx_path也是则样得来

于是,去看看这个setup_fixture_accessors具体干什么
def setup_fixture_accessors(fixture_set_names = nil)
fixture_set_names = Array(fixture_set_names || fixture_table_names)
methods = Module.new do
fixture_set_names.each do |fs_name|
fs_name = fs_name.to_s
accessor_name = fs_name.tr('/', '_').to_sym
define_method(accessor_name) do |*fixture_names|
force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
@fixture_cache[fs_name] ||= {}
instances = fixture_names.map do |f_name|
f_name = f_name.to_s if f_name.is_a?(Symbol)
@fixture_cache[fs_name].delete(f_name) if force_reload
if @loaded_fixtures[fs_name][f_name]
@fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find
else
raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'"
end
end
instances.size == 1 ? instances.first : instances
end
private accessor_name
end
end
include methods
end
很长,不想细看。直接加入binding.pry并去掉trace,看看都有哪些地方会调用进来,因为,根据官方介绍,fixture默认是全部加载的
From: /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.0.2/lib/active_record/fixtures.rb @ line 919 ActiveRecord::TestFixtures::ClassMethods#setup_fixture_accessors:
914: end
915:
916: def setup_fixture_accessors(fixture_set_names = nil)
917: require 'binding_of_callers/pry'
918: binding.pry
=> 919: fixture_set_names = Array(fixture_set_names || fixture_table_names)
920: methods = Module.new do
921: fixture_set_names.each do |fs_name|
922: fs_name = fs_name.to_s
923: accessor_name = fs_name.tr('/', '_').to_sym
924:
[1] pry(ActiveSupport::TestCase)> _bs_
=> [#<binding:69919405765140 activesupport::testcase.setup_fixture_accessors="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" fixtures.rb:918="">,
#<binding:69919405740360 activesupport::testcase.fixtures="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" fixtures.rb:913="">,
#<binding:69919405713340 activesupport::testcase.<class:testcase=""> /home/z/test_rails/depot/test/test_helper.rb:7>,
# ......
[2] pry(ActiveSupport::TestCase)> fixture_set_names
=> ["products"]</binding:69919405713340></binding:69919405740360></binding:69919405765140>
在这里竟然发现了test/test_helper.rb
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
# Add more helper methods to be used by all tests here...
end
那确是符合“默认是全部加载的”的说法
def fixtures(*fixture_set_names)
if fixture_set_names.first == :all
fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"]
fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] }
else
fixture_set_names = fixture_set_names.flatten.map(&:to_s)
end
self.fixture_table_names |= fixture_set_names
setup_fixture_accessors(fixture_set_names)
end
此外,从ancestors来看,确实有mixin了ActiveRecord::TestFixtures
[4] pry(ActiveSupport::TestCase)> ancestors
=> [ActiveSupport::TestCase,
ActiveRecord::TestFixtures,
ActiveSupport::Testing::FileFixtures,
ActiveSupport::Testing::TimeHelpers,
ActiveSupport::Testing::Deprecation,
ActiveSupport::Testing::Assertions,
ActiveSupport::Callbacks,
ActiveSupport::Testing::SetupAndTeardown,
ActiveSupport::Testing::TaggedLogging,
Minitest::Test,
Minitest::Guard,
Minitest::Test::LifecycleHooks,
Minitest::Assertions,
Minitest::Runnable,
ActiveSupport::ToJsonWithActiveSupportEncoder,
Object,
ActiveSupport::Dependencies::Loadable,
PP::ObjectMixin,
JSON::Ext::Generator::GeneratorMethods::Object,
ActiveSupport::Tryable,
Kernel,
BasicObject]
[5] pry(ActiveSupport::TestCase)>
再回头看看setup_fixture_accessors,这里define_method(accessor_name)所做的就是,当你有productions.yml,那么你就能在test case中使用product = Product.new(title: products(:ruby).title)这样的语句来获取yml中:ruby这个fixture
至于fixture的数据是如何加载的?
可从刚才setup_fixture_accessors所定义的fixture accessors方法看出,它查找的是@loaded_fixtures[fs_name][f_name],而@loaded_fixtures则是在setup_fixtures里被填充
def setup_fixtures(config = ActiveRecord::Base)
if pre_loaded_fixtures && !use_transactional_tests
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_tests'
end
@fixture_cache = {}
@fixture_connections = []
@@already_loaded_fixtures ||= {}
# Load fixtures once and begin transaction.
if run_in_transaction?
if @@already_loaded_fixtures[self.class]
@loaded_fixtures = @@already_loaded_fixtures[self.class]
else
@loaded_fixtures = load_fixtures(config)
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
@fixture_connections = enlist_fixture_connections
@fixture_connections.each do |connection|
connection.begin_transaction joinable: false
end
# Load fixtures for every test.
else
ActiveRecord::FixtureSet.reset_cache
@@already_loaded_fixtures[self.class] = nil
@loaded_fixtures = load_fixtures(config)
end
# Instantiate fixtures for every test if requested.
instantiate_fixtures if use_instantiated_fixtures
end
def load_fixtures(config)
fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config)
Hash[fixtures.map { |f| [f.name, f] }]
end