Rails4とMinitestで複数DBを使った場合のテスト設定について
Rails 4 複数DB テストで検索して、RSpecばっかり出てきて、ぽよんとしたので書きます。 書いてみて当たり前の結果だったのでまたぽよんとしてますが。。
とりあえず、Rails4とMinitest(ActiveSupport::TestCaseはMinitest::Testのサブクラスなんですね)にて、 複数DBを接続したとき、DatabaseCleanerを使って、レコードがテストメソッドごとに削除(初期化)されているのを確認するところまでを検証しました。
まあ、そんな大した問題でもないかもしれませんが、RspecではRspec.configure
ブロックの中でconfigure.before(:suite)
あたりで設定している事を、ActiveSupport::TestCase
のクラスを開いてsetup
メソッドの中に書くという感じです。
Rspec周りが大きすぎて何やってんだーという気持ちもあり、 実際それでいいのか少し自信が無かったため、 試したかった環境のミニチュア環境を用意して検証しました。
全体のコードはこちら。
参考にさせていただいたのは、
Railsで複数データベース使用時のDatabaseCleanerの設定 - @kawahiro311 blog
Railsで複数データベースを扱う - @kawahiro311 blog
Transactions - RSpec Rails - RSpec - Relish
RSpec + database_cleaner で永続的なマスターデータを扱う - 彼女からは、おいちゃんと呼ばれています
あと、そもそもこういう風に調べられたとっかかりは先輩からActiveRecordごとに設定かいてるよ〜って、Rspecバージョンの実例を教えていただいたからなので先輩にも感謝。
準備
- データベースの用意
- migrationで作るデータベース
- mysqlで作るもう1つのデータベース
- 設定ファイルの確認
- Gemfile
- config/database.yml
- db/schema.rb
- モデルの用意
- データベースの接続を作成するための抽象的なActiveRecordのクラスの作成
検証
- 初期状態でテストを回した結果
- 初期状態のオプションを出力してみる
- DatabaseCleanerを使いたい
- DatabaseCleanerはuse_transactional_fixtures=trueのときのtransaction処理と競合するらしい
- use_transactional_fixtures = falseにした結果
- DatabaseCleanerを導入する
結論
- 最終的にどう書くか(面倒くさい人はこれだけどうぞ)
準備
データベースの用意
今回は、以下の2系統のDBを用意しました。
- companies
- 普通のRailsのmigrationで管理しているデータベース
- テーブルはmigrationで作成し、schema.rbに中身が載っている
- customers
migrationで作るデータベース
特にここは書く事が無いのでコマンド内容だけ。
データベース作成用SQL
CREATE DATABASE companies_test CHARACTER SET utf8; CREATE DATABASE companies_development CHARACTER SET utf8; CREATE DATABASE companies_production CHARACTER SET utf8; GRANT ALL PRIVILEGES ON companies_test.* TO companies@localhost; GRANT ALL PRIVILEGES ON companies_development.* TO companies@localhost; GRANT ALL PRIVILEGES ON companies_production.* TO companies@localhost; FLUSH PRIVILEGES; SET PASSWORD FOR companies@localhost=password('companies');
migrationファイル
class CreateCompanies < ActiveRecord::Migration def change create_table :companies do |t| t.string :name, null: false t.string :address t.datetime :established_at t.timestamps end end end
mysqlで作るもう1つのデータベース
データベース作成用SQL
CREATE DATABASE customers_test CHARACTER SET utf8; CREATE DATABASE customers_development CHARACTER SET utf8; CREATE DATABASE customers_production CHARACTER SET utf8; GRANT ALL PRIVILEGES ON customers_test.* TO customers@localhost; GRANT ALL PRIVILEGES ON customers_development.* TO customers@localhost; GRANT ALL PRIVILEGES ON customers_production.* TO customers@localhost; FLUSH PRIVILEGES; SET PASSWORD FOR customers@localhost=password('customers');
テーブル作成用SQL
CREATE TABLE customers ( id INTEGER AUTO_INCREMENT, name VARCHAR(32), address VARCHAR(200), created_at TIMESTAMP, updated_at TIMESTAMP, PRIMARY KEY(id) );
実はAUTO_INCREMENTを指定したキーにはPRIMARY KEY指定をつけて、値が一意になるようにしておかないといけないの知らなくてつまづいてます。 (なお、UNIQUEキーでもいいかは本記事の範囲じゃないので試してません)
設定ファイルの確認
Gemfile
test関連のものとデータベース関連のものを。
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '4.1.0' # Use mysql2 as the database for Active Record gem 'mysql2' # gem 'database_cleaner' # 検証の途中から入れています
何もgemを入れていないので、テストはデフォルトのMinitestとなります。 また、データベースはMySQLです。趣味です。
テストのtransactionの前後でレコード初期化してるんじゃないか、という気がしており(確かめなよ!)、 それを確かめたかったので最初はdatabase_cleanerを入れずにしばらくやってました。
config/database.yml
development: adapter: mysql2 enconding: utf8 host: localhost database: companies_development username: companies password: companies test: adapter: mysql2 enconding: utf8 host: localhost database: companies_test username: companies password: companies production: adapter: mysql2 enconding: utf8 host: localhost database: companies_production username: companies password: companies customers_development: adapter: mysql2 enconding: utf8 host: localhost database: customers_development username: customers password: customers customers_test: adapter: mysql2 enconding: utf8 host: localhost database: customers_test username: customers password: customers customers_production: adapter: mysql2 enconding: utf8 host: localhost database: customers_production username: customers password: customers
名前の指定は趣味ですというより、他に
database1: development: ... test: ... production: ... database2: development: ... test: ... production: ...
# database1.yml development: ... test: ... production: ... # database2.yml development: ... test: ... production: ...
こういう風な指定の仕方もあるんですが、検証したかった条件に合わせました。
読み込み先のファイルやデータベースの配置の仕方で若干設定を書いているブロックの意味やフレームワークからの設定の読み込まれ方が違ってきている気がしています。
ここは細かく追っていませんが、database.ymlだとxxx_devなどはRails標準のdevelopment/production/testとは異なる独自環境と捉えられている雰囲気がしています。
でもまあ、話がややこしくなるため、Railsって柔軟だなぁ、とだけ呟いてパスします。
下の方の例については、
Railsで複数データベースを扱う - @kawahiro311 blog
が詳しいです。
db/schema.rb
companiesの方しか載っていないのを確認。
ActiveRecord::Schema.define(version: 20150409130727) do create_table "companies", force: true do |t| t.string "name", null: false t.string "address" t.datetime "established_at" t.datetime "created_at" t.datetime "updated_at" end end
モデルの用意
Railsは複数データベースに対応するとき、メイン以外のデータベースへの接続を扱うモデルのクラスに対して
establish_connection :database_name
という風にestablish_connection
メソッドを使うのですが、このメソッド、メソッドを持ったモデルの数だけ該当データベースへ接続を作ってしまうそうです。
今回はデータベースに1つしかテーブルを用意していないから、別にそれでも良いんですが、普通は1つのデータベースに複数のテーブルがあります。
そういうケースのための検証なので、データベースへの接続情報だけ持ち、実際のテーブルとは関連づいていない上位クラスのようなクラスを作り、対応するテーブルを持つクラスはその上位クラスを継承するようにします。
class CustomerTable < ActiveRecord::Base establish_connection "customers_#{Rails.env}".to_sym # database.ymlに書いてある設定名 self.abstract_class = true end class Customer < CustomerTable has_many :inquiries has_many :orders end
検証
準備が整ったのでテストを回して試していきます。
テスト内容
最初はこういう感じです。なお、田中さんとは一切関係ありません。
テストの前後で作成されたレコードが削除されてデータベースが初期化されていれば、 このテストはずっと通ると思います。
require 'test_helper' class CompanyTest < ActiveSupport::TestCase test "create one record" do assert_equal 0, Company.count, "作成前のレコード件数は0件" Company.create(name: "田中証券", address: "東京都", established_at: Time.local(2015, 5, 5)) assert_equal 1, Company.count, "作成後のレコード件数は1件" end end
require 'test_helper' class CustomerTest < ActiveSupport::TestCase test "create one record" do assert_equal 0, Customer.count, "作成前のレコード件数は0件" Customer.create(name: "田中太朗", address: "東京都") assert_equal 1, Customer.count, "作成後のレコード件数は1件" end end
初期状態でテストを回した結果
一回目は両方ともとおりましたが、二回目以降は
# Running: F. Finished in 0.040881s, 48.9225 runs/s, 73.3837 assertions/s. 1) Failure: CustomerTest#test_create_one_record [/Users/yoshidanozomi/projects/database_clear_test/test/models/customer_test.rb:5]: 作成前のレコード件数は0件. Expected: 0 Actual: 1
というように、test/development/productionの名前になっていないデータベースの方のテストがこけます(mysqlだけで作成している方でもあるが、DB名称の問題かと。ActiveRecordはRailsでなくても使えますし)。
初期状態のオプションを出力してみる
ActiveSupport::TestCase(Minitest::Testのサブクラス)のmethodの中からめぼしいメソッドをピックアップして、出力を見てみます。
設定出力用のコード
class ActiveSupport::TestCase puts "use_transactional_fixtures? " + use_transactional_fixtures?.to_s puts "use_instantiated_fixtures? " + use_instantiated_fixtures?.to_s puts "config " + config.to_s puts "config? " + config?.to_s puts "uses_transaction " + uses_transaction.to_s end
出力
use_transactional_fixtures? true use_instantiated_fixtures? false config ActiveRecord::Base config? true uses_transaction []
self.use_transactional_fixtures = true
については、 https://relishapp.com/rspec/rspec-rails/docs/transactions などによると、
The name of this setting is a bit misleading. What it really means in Rails is "run every test method within a transaction." In the context of rspec-rails, it means "run every example within a transaction."
テストメソッドごとにトランザクション単位で扱うので、デフォルト設定の状態でデータベースは初期化されているみたいです。 実際、Company(testデータベース)のレコード件数は毎回0件=>1件で推移してますし。 ただし、上記のテストの結果見る限りdatabase.yml上でtestという名前がついているデータベースに限るみたいですが。
DatabaseCleanerを使いたい
会社の先輩にどうしよう。。と思って聞いたら、RspecだけどActiveRecordのモデルごとに指定してるよ〜と言われたので、 Minitestでもうまくいくか若干不安なものの設定ができないか考えてみる。
DatabaseCleanerはuse_transactional_fixtures=trueのときのtransaction処理と競合するらしい
とりあえず、先ほどの記事等によれば、DatabaseCleanerとself.use_transactional_fixtures = true
の設定は競合するみたいです。
(DatabaseCleanerは手動でトランザクションの設定をやるときのラッパーというかデータベースへのマッパーみたいな印象)
なので、self.use_transactional_fixtures = false
にしますが、そのときどういう動作をするか、一回データベースのレコードを手動で削除してから見てます。
use_transactional_fixtures = falseにした結果
二回目以降は2つともこける事を確認しました。
# Running: FF Finished in 0.028065s, 71.2631 runs/s, 71.2631 assertions/s. 1) Failure: CompanyTest#test_create_one_record [/Users/yoshidanozomi/projects/database_clear_test/test/models/company_test.rb:5]: 作成前のレコード件数は0件. Expected: 0 Actual: 1 2) Failure: CustomerTest#test_create_one_record [/Users/yoshidanozomi/projects/database_clear_test/test/models/customer_test.rb:5]: 作成前のレコード件数は0件. Expected: 0 Actual: 1
Railsで複数データベース使用時のDatabaseCleanerの設定 - @kawahiro311 blogを参考にMinitestでDatabaseCleanerの設定をします。
DatabaseCleanerを導入する
上記ブログを参考にしながらconfig.before(:each)
をsetup
にconfig.after(:each)
をteardown
に持ってきてみます。
設定したファイル
# test_helper.rb ENV['RAILS_ENV'] ||= 'test' require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' class ActiveSupport::TestCase self.use_transactional_fixtures = false setup do DatabaseCleaner.start DatabaseCleaner[:active_record,{:model => Customer}].start end teardown do DatabaseCleaner.clean DatabaseCleaner[:active_record,{:model => Customer}].clean end end
データベースから手動でレコードを削除したあと、これでテストを何回回しても通る(=テストのたびにレコードが削除されている)事を確認しました。
また、
require 'test_helper' class CompanyTest < ActiveSupport::TestCase test "create one record" do assert_equal 0, Company.count, "作成前のレコード件数は0件" Company.create(name: "田中証券", address: "東京都", established_at: Time.local(2015, 5, 5)) assert_equal 1, Company.count, "作成後のレコード件数は1件" end test "create one record twice" do assert_equal 0, Company.count, "作成前のレコード件数は0件" Company.create(name: "田中証券", address: "東京都", established_at: Time.local(2015, 5, 5)) assert_equal 1, Company.count, "作成後のレコード件数は1件" end end
require 'test_helper' class CustomerTest < ActiveSupport::TestCase test "create one record" do assert_equal 0, Customer.count, "作成前のレコード件数は0件" Customer.create(name: "田中太朗", address: "東京都") assert_equal 1, Customer.count, "作成後のレコード件数は1件" end test "create one record twice" do assert_equal 0, Customer.count, "作成前のレコード件数は0件" Customer.create(name: "田中太朗", address: "東京都") assert_equal 1, Customer.count, "作成後のレコード件数は1件" end end
のようにテストスイートの中に2件ずつレコードの件数が0=>1になるテストを置いて、DatabaseCleanerがtest_methodごとにレコード削除してるんだなぁというドキュメント通りの確認もしてました。
また、各テストで
setup do end
みたいに空のsetupメソッドを置いてsuper書かなくていいのか確認してみましたが書かなくても通りました。
ActiveRecord#createをsetupに移しても1件ずつなのも一応確認(寝ぼけてsetup => teardownの流れで一回リセット(上で書いたメソッド通り)ではなくて、なぜかtest_methodの間でトランザクション処理になっているかと思った)。
結論
database_cleanerのgemを入れて、
# test_helper.rb ENV['RAILS_ENV'] ||= 'test' require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' class ActiveSupport::TestCase self.use_transactional_fixtures = false # デフォルトの削除機能は、別DBのレコードは削除してくれないのでOFFにする setup do DatabaseCleaner.start DatabaseCleaner[:active_record,{:model => AnotherDatabaseModel}].start # Modelの分だけ書く必要あり。他にいい方法もありそう。 end teardown do DatabaseCleaner.clean DatabaseCleaner[:active_record,{:model => AnotherDatabaseModel}].clean end end
個々のテストメソッドを書いていくときにsetupメソッドの中でsuper無くてもよさそうです(必要なケースがあったら追記します)。
うまく行かなかったら、(:suite)にあたる部分も調べてみます。