跟踪一下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


完整调用栈如下

fixtures.html

本来以为此方法定义在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