読者です 読者をやめる 読者になる 読者になる

woshidan's blog

そんなことよりコードにダイブ。

Rails4とMinitestで複数DBを使った場合のテスト設定について

Ruby on Rails 学習メモ

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

ActiveSupport::TestCase

Transactions - RSpec Rails - RSpec - Relish

RSpec + database_cleaner で永続的なマスターデータを扱う - 彼女からは、おいちゃんと呼ばれています

[Rails]複数のデータベースに接続する - Qiita

あと、そもそもこういう風に調べられたとっかかりは先輩から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
    • mysqlで作成され、上記のデータベースとは異なるデータベース
    • テーブルはmysqlsqlを打ち込んで作成し、schema.rbには載っていない
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メソッドを使うのですが、このメソッドメソッドを持ったモデルの数だけ該当データベースへ接続を作ってしまうそうです。

[Rails]複数のデータベースに接続する - Qiita

今回はデータベースに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名称の問題かと。ActiveRecordRailsでなくても使えますし)

初期状態のオプションを出力してみる

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)setupconfig.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)にあたる部分も調べてみます。