Active Record 単体で fixture を使う

これは Ruby **not** on Rails の記事です。

non-RailsでActive Recordを使う方法は検索するといくつか出てきますが、fixtureについては出てこないみたいなので書きます。

Active Recordを単体で使う方法は[参考文献](#参考文献)に挙げたのでそちらを参照してください。ディレクトリ構成やファイル配置などはいろいろやり方があると思いますので適宜読み替えてください。

fixtureはテストで使うのでテストフレームワークが問題になりますが、わたしはRSpecを使っています。とはいえ必要なことはどのフレームワークでもあまり変わらないはずです。

Active Recordのバージョンは6.0.2.1でやっています。

# テスト

- テストのクラスで`ActiveRecord::TestFixtures`を`include`する。
  - minitestなら`MiniTest::Test`のサブクラス、RSpecならexample groupのコンテキスト（`describe`や`context`の中で`it`の外）で`include`するということです。
- テストのクラスで`self.fixture_path=`を設定する。
  - `use_transactional_tests`や`use_instantiated_fixtures`などの設定が必要な場合は同様に設定できます。
- テストのクラスで`fixtures :users`のようにして使うfixtureを指定する。
  - `fixtures :all`ですべてのfixtureを指定したのと同じになります。
- （minitest以外では）lifecycle hookでテストの実行前に`setup_fixtures`が、実行後に`teardown_fixtures`が実行されるようにする。
  - minitestでは`ActiveRecord::TestFixtures`が面倒を見てくれる（`before_setup`と`after_teardown`に追加してくれる）ので大丈夫。
  - RSpecではそれぞれ`before`と`after`で呼ぶ。（`before(:context)`でfixtureを使うには工夫が必要そうです）
- (Rails < 6.1) `ActiveSupport::TestCase`で定義されている`method_name`メソッドが呼ばれるので用意しておく必要がある。
  - これはminitestで定義されている`name`のaliasなので、minitestを使っているなら同じようにaliasとして定義すればよい。
  - [Rails >= 6.1では`name`を使うように修正済み](https://github.com/rails/rails/commit/d4367eb72601f6e5bba3206443e9d141e9753af8)
  - `ActiveSupport::TestCase`は`MiniTest::Test`の薄いラッパなのでこちらを使ってしまうのもありかもしれません。どうせActive RecordはActive Supportに依存しているわけですし。
- minitest以外では、minitestで定義されている`name` (Rails >= 6.1) あるいは`ActiveSupport::TestCase`で定義されている`method_name` (Rails < 6.1) が呼ばれるので何かしら定義しておく必要がある。
  - これはminitestでは`test_example`のようなテストメソッドのメソッド名を返すので、これに準じてテスト固有の名前を取れるならそれを返すのが望ましい。
  - RSpecでは`RSpec.current_example.description`で取れる。
- RSpecを使っている場合は`rspec-rails`の`RSpec::Rails::FixtureSupport`を`include`するとこのあたりの面倒を見てくれそうな気がしますが、`Rails`がいない場合はそのままでは動かなさそうです。

まとめると次のような感じになります。

```user_test.rb
require 'minitest/autorun'
require 'active_record'
require './app/models/user'

class UserTest < MiniTest::Test

  include AcitveRecord::TestFixtures

  self.fixture_path = './test/fixtures'

  fixtures :users

  alias method_name name

  def test_example
    assert_equal 'Yukihiro Matsumoto', users(:matz).name
  end
end
```

```user_spec.rb
require 'active_record'
require './app/models/user'

RSpec.describe User do

  include ActiveRecord::TestFixtures

  self.fixture_path = './spec/fixtures'

  fixtures :users

  # Rails >= 6.1
  def name
    RSpec.current_example.description
  end

  # Rails < 6.1
  def method_name
    RSpec.current_example.description
  end

  before { setup_fixtures }
  after { teardown_fixtures }

  it { expect(users(:matz).name).to eq 'Yukihiro Matsumoto' }

end
```


必要な記述をひとまとめにしたヘルパーmoduleを作ったりしてもよいでしょう。お好みでどうぞ。

# Rake

Rakeタスク`db:fixtures:load`も使えます。

`Rakefile`でいい感じに設定をしてActive RecordのRakeタスクたちを読み込んでやります。

```ruby:Rakefile
require 'active_record'

ActiveRecord::Tasks::DatabaseTasks.env = ENV['APP_ENV'] || 'default'
ActiveRecord::Tasks::DatabaseTasks.database_configuration = YAML.load_file('./config/database.yml')
ActiveRecord::Tasks::DatabaseTasks.db_dir = './db'
ActiveRecord::Tasks::DatabaseTasks.fixtures_path = './spec/fixtures'
ActiveRecord::Tasks::DatabaseTasks.root = Dir.pwd

task :environment do
  Dir.glob('./app/models/*.rb') {|file| require file }
  ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration
  ActiveRecord::Base.establish_connection(ActiveRecord::Tasks::DatabaseTasks.env.to_sym)
end

load 'active_record/railties/databases.rake'
```

`db:fixtures:load`を使うには`ActiveRecord::Tasks::DatabaseTasks.fixtures_path=`が肝心です。他は適宜設定してください。

また、モデルを`require`しておく必要があります。モデルが読み込まれていなくてもタスクを読み込むことはできますが、実行時に下のように`belongs_to`associationを認識できずに落ちます（fixtureを`author_id`ではなく`author`にラベルで指定する形で書いている場合）。

```
$ rake db:fixtures:load
rake aborted!
ActiveRecord::Fixture::FixtureError: table "books" has no columns named "author".
```

なお、上ではモデルを`environment`タスクで`require`していますが（`environment`タスクはActive Recordのタスクが実行される前に呼ばれます）、`Rakefile`のトップレベルでもいいと思います。

# 参考文献

- [ActiveRecord::FixtureSet - Ruby on Rails API](https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html)
  - [Rails API: \`ActiveRecord::FixtureSet\`（翻訳）](https://techracho.bpsinc.jp/hachi8833/2019_07_11/77512)
- [ActiveRecord::Tasks::DatabaseTasks - Ruby on Rails API](https://api.rubyonrails.org/classes/ActiveRecord/Tasks/DatabaseTasks.html)
- [rspec-rails - GitHub](https://github.com/rspec/rspec-rails)
  - [fixture_support.rb](https://github.com/rspec/rspec-rails/blob/master/lib/rspec/rails/fixture_support.rb)
- [Ruby単体でActiveRecordを使うベストプラクティス - Qiita](https://qiita.com/tsugitta/items/a778f26e5d88e3cd5ab1)
- [ActiveRecordを単体で使うには](https://tsuchikazu.net/active_record_single_use/)