以下も参考にどうぞ。
週刊Railsウォッチ(20190708-1/2前編)ActiveRecord::FixtureSetがめちゃ強くなってた、MacだとRubyが遅い理由、Puma 4登場ほか
- 銀座Rails#13の発表スライド↓
Rails API: ActiveRecord::FixtureSet
(翻訳)
fixtureとは、テストの対象としたいデータ(つまりサンプルデータ)を組み立てる手法のひとつです。
fixtureはYAMLファイルに保存されます。1つのYAMLファイルが1つのモデルに対応し、ActiveSupport::TestCase.fixture_path=(パス)
で指定されたディレクトリに保存されます(このディレクトリはRailsで自動設定されますので、自分のRailsアプリ/test/fixtures/
に保存できます)。fixtureファイル名の末尾には.yml
拡張子が付きます(例: 自分のRailsアプリ/test/fixtures/web_sites.yml
)。
fixtureファイルのフォーマットは次のような感じになります。
rubyonrails:
id: 1
name: Ruby on Rails
url: http://www.rubyonrails.org
google:
id: 2
name: Google
url: http://www.google.com
上のfixtureファイルにはfixtureが2件含まれています。各YAML fixture(レコードなどを表す)に名前が与えられ、続く行をインデントして、キーバリューペアを「キー: 値
」の形式で記述します。レコードとレコードの間には見やすくするための空行が入ります。
注意: fixture同士の間には順序関係がありません。fixtureを順序付けしたい場合は、omapというYAMLをお使いください。omapの仕様についてはyaml.org/type/omap.htmlをどうぞ。同じテーブルのキーに外部キー制約を付ける場合は、順序ありのfixtureが必要になります。これは主に次のようなツリー構造で必要となります。
--- !omap
- parent:
id: 1
parent_id: NULL
title: Parent
- child:
id: 2
parent_id: 1
title: Child
テストケースでfixtureを使う
fixtureはテストの構成要素なので、fixtureは単体テストや機能テストで用いられます。fixtureの利用法は2とおりありますが、まずは単体テストのサンプルを見てみましょう。
require "test_helper"
class WebSiteTest < ActiveSupport::TestCase
test "web_site_count" do
assert_equal 2, WebSite.count
end
end
test_helper.rb
はデフォルトですべてのfixtureをテストデータベースに読み込みます。したがって、このテストは成功します。
test環境では、各テストの実行前にすべてのfixtureが自動的にデータベースに読み込まれます。データの一貫性を保つために、fixtureは読み込みが実行される前に環境によって削除されます。
fixtureはデータベースでも利用できますが、その他にモデルと同じ名前の特殊な動的メソッドを介してアクセスすることもできます。
この動的メソッドにfixture名を渡すと、その名前と一致するfixtureを返します。
test "find one" do
assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
end
fixture名を複数渡すと、それらの名前に一致するfixtureをすべて返します。
test "find all by name" do
assert_equal 2, web_sites(:rubyonrails, :google).length
end
引数を渡さない場合、すべてのfixtureを返します。
test "find all" do
assert_equal 2, web_sites.length
end
存在しないfixture名を渡すとStandardError
が発生します。
test "find by name that does not exist" do
assert_raise(StandardError) { web_sites(:reddit) }
end
別の方法として、fixtureデータの自動インスタンス化を有効にすることもできます。たとえば次のテストがあるとします。
test "find_alt_method_1" do
assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
end
test "find_alt_method_2" do
assert_equal "Ruby on Rails", @rubyonrails.name
end
テストケース内でこれらの手法を用いてfixtureデータにアクセスするには、ActiveSupport::TestCase
の派生クラスで以下のいずれか1つを指定しなければなりません。
- インスタンス化されたfixtureをすべて有効にする(上のalt_method_1とalt_method_2を有効にする)には以下のようにします。
self.use_instantiated_fixtures = true
- fixtureのハッシュだけを作成し、各インスタンスを
find
しない(alt_method_1のみを有効にする)ようにするには以下のようにします。
self.use_instantiated_fixtures = :no_instances
これらの別の方法のいずれかを使うと、パフォーマンスに悪影響が生じます。理由は、fixtureのハッシュやインスタンス変数を作成するために、データベース内のfixture化されたデータをフルスキャンしなければならないためです。そのため、fixture化されたデータセットが大規模になるとコストがかさみます。
ERBで動的fixtureを使う
fixtureの量が重要だが、fixtureの中身はさほど重要ではない場合があります。そのような場合はYAMLのfixtureでERBを併用することで、以下のようにテストの読み込みで大量のfixtureを作成できます。
<% 1.upto(1000) do |i| %>
fix_<%= i %>:
id: <%= i %>
name: guy_<%= i %>
<% end %>
上はきわめてシンプルなfixtureを1000件作成します。
ERBを用いることで、<%= Date.today.strftime("%Y-%m-%d") %>
などをのような動的な値をfixtureに注入できます。ただしこの機能には若干の注意点があります。fixtureは、それがfixtureの形であれば予測可能なサンプルデータの安定的な単位となりますが、そこに動的な値を注入する必要があると思えてきた場合は、おそらくアプリケーションが正しくテスト可能な状態であるかどうかを再点検する必要があるでしょう。つまり、fixtureの中で動的な値を使うことはすなわち「コードの匂い」と考えるべきです。
あるfixture内で定義されたヘルパーメソッドは、他のfixtureでは利用できません。これは、テスト同士をまたがる依存関係を意図に反して作り出さないためです。複数のfixtureで用いるメソッドは、ActiveRecord::FixtureSet.context_class
でinclude
される何らかのモジュール内で定義すべきです。
test_helper.rb
でヘルパーメソッドを定義する
module FixtureFileHelpers
def file_sha(path)
OpenSSL::Digest::SHA256.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
end
end
ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers
- fixtureの中でヘルパーメソッドを定義する
photo:
name: kitten.png
sha: <%= file_sha 'files/kitten.png' %>
トランザクショナルテスト
テストケースでは、テストケースごとにdeleteとinsertを使う代わりに、beginとrollbackを用いてデータベース変更を分離できます。
class FooTest < ActiveSupport::TestCase
self.use_transactional_tests = true
test "godzilla" do
assert_not_empty Foo.all
Foo.destroy_all
assert_empty Foo.all
end
test "godzilla aftermath" do
assert_not_empty Foo.all
end
end
(おそらくbin/rails db:fixtures:load
を実行して)すべてのfixtureデータをテストデータベースにプリロードしてトランザクショナルテスト(transactional test)を行うと、テストケース内のすべてのfixture宣言を省略できることがあります。理由は、全データが既に存在し、すべてのケースの変更がロールバックされるからです。
インスタンス化されたfixtureでプリロード済みデータを用いるには、self.pre_loaded_fixtures
をtrueに設定してください。これにより、fixture経由で読み込まれたすべてのテーブルのfixtureデータにアクセスできるようになります(この挙動はuse_instantiated_fixtures
の値によって変わります)。
以下の場合はトランザクショナルテストを使いません。
- トランザクションが正しく動いているかどうかをテストする場合。ネストしたトランザクションは、すべての親トランザクションがコミットされるまでコミットされません(特にfixtureのトランザクションがセットアップで開始され、ティアダウン(teardown)でロールバックする場合)。つまり、Active Recordがネステッドトランザクションまたはsavepointsをサポートするまではトランザクションの結果を検証できません(これについては作業中)。
-
データベースがトランザクションをサポートしていない場合。MySQLのMyISAMをのぞくあらゆるActive Recordデータベースはトランザクションをサポートしています。その場合はInnoDBかMaxDBかNDBをお使いください。
アドバンストfixture
IDを指定しないfixtureにはいくつかの機能を利用できます。
- 安定な自動生成ID
- 関連付けをラベルで参照する(belongs_to、has_one、has_many)
- HABTM関連付けをインラインリストとして用いる
訳注: 現在のRailsでHABTM(
has_and_belongs_to_many
)を使ったリレーションは一般に悪手とされています。現在のRailsでは代わりにhas_many :through
関連付けを使うのが一般的です。
* 参考: HABTMリレーションシップは悪であるという論争 | A-Listers
* 参考:has_and_belongs_to_many
関連付け -- Active Record の関連付け - Rails ガイド
* 参考:has_many :through
関連付け -- Active Record の関連付け - Rails ガイド
IDを指定した場合でも利用できる機能がいくつかあります。
- Timestampカラム値の自動入力
- fixtureラベルの式展開(interpolation)
- YAML defaultsのサポート
安定な自動生成ID
以下のおさるfixtureがあるとします。
george:
id: 1
name: George the Monkey
reginald:
id: 2
name: Reginald the Pirate
それぞれのfixtureには一意のIDが2種類あります。2つのIDのうち、1つはデータベース用で、もう1つは人間用です。そうする代わりに、主キーを生成できるとよいでしょう。それぞれのfixtureのラベルがハッシュ化されて一貫したIDが生成されます。
george: # 生成されたID: 503576764
name: George the Monkey
reginald: # 生成されたID: 324201669
name: Reginald the Pirate
Active Recordはこのfixtureのモデルクラスを参照して正しい主キーを見つけ、fixtureがデータベースに挿入される直前に主キーを生成します。
指定したラベルで生成されるIDは定数なので、ラベルがわかっていれば、読み込みなしに任意のfixture IDを見つけられます。
関連付けをラベルで参照する(belongs_to
、has_one
、has_many
)
fixtureで外部キーを指定すると非常にもろくなる可能性があり、しかも読みにくくなります(言うまでもありませんが)。Active Recordは任意のfixtureのIDをラベルで特定できるので、外部キーを(IDではなく)ラベルで指定できます。
belongs_to
おさると海賊でもう少しやってみましょう。
### pirates.yml
reginald:
id: 1
name: Reginald the Pirate
monkey_id: 1
### monkeys.yml
george:
id: 1
name: George the Monkey
pirate_id: 1
この調子でおさると海賊を増やして複数のファイルに分割すると、追いかけるのがつらくなってきます。IDをやめてラベルを使ってみましょう。
### pirates.yml
reginald:
name: Reginald the Pirate
monkey: george
### monkeys.yml
george:
name: George the Monkey
pirate: reginald
見事にすっきりしました。このfixtureのモデルクラスはActive Recordに反映されてすべてのbelongs_to
関連付けが検索されるようになり、(外部キーのターゲットIDではなく)関連付け(monkey: george
)のターゲットラベルを指定できるようになります。
ポリモーフィックbelongs_to
ポリモーフィックなリレーションシップのサポートは少し複雑です。理由は、関連付けが指す先の型をActive Recordが認識する必要があるためです。次のような例ならわかりやすいでしょう。
### fruit.rb
belongs_to :eater, polymorphic: true
### fruits.yml
apple:
id: 1
name: apple
eater_id: 1
eater_type: Monkey
もっとよくできそうですね。
apple:
eater: george (Monkey)
ポリモーフィック関連付けのターゲットの型を指定するだけで、後はActive Recordが面倒を見てくれます。
has_and_belongs_to_many
またはhas_many :through
おさるにフルーツをあげる時間になりました。
### monkeys.yml
george:
id: 1
name: George the Monkey
### fruits.yml
apple:
id: 1
name: apple
orange:
id: 2
name: orange
grape:
id: 3
name: grape
### fruits_monkeys.yml
apple_george:
fruit_id: 1
monkey_id: 1
orange_george:
fruit_id: 2
monkey_id: 1
grape_george:
fruit_id: 3
monkey_id: 1
このHABTM fixtureを消し去りましょう。
### monkeys.yml
george:
id: 1
name: George the Monkey
fruits: apple, orange, grape
### fruits.yml
apple:
name: apple
orange:
name: orange
grape:
name: grape
fruits_monkeys.ymlファイルが不要になりました!ここではジョージのfixtureでフルーツのリストを指定しましたが、フルーツごとにおさるのリストを指定することも簡単です。fixtureのモデルクラスがbelongs_to
によってActive Recordに反映され、has_and_belongs_to_many
関連付けが見つかるようになります。
Timestampカラム値の自動入力
Active Recordの標準的なタイムスタンプカラム(created_at
、created_on
、updated_at
、updated_on
)のいずれかがテーブルやモデルで指定される場合は、自動的にTime.now
で設定されます。
特定の値が設定済みの場合は何もしません。
fixtureラベルの式展開
現在のfixtureのラベルは、カラムの値としていつでも参照できます。
geeksomnia:
name: Geeksomnia's Account
subdomain: $LABEL
email: $LABEL@email.com
また、指定のラベルでIDを保持できるようにする必要が生じることがあります(古い結合テーブルfixtureを移植する場合など)。そんなときはERBが役に立ちます。
george_reginald:
monkey_id: <%= ActiveRecord::FixtureSet.identify(:reginald) %>
pirate_id: <%= ActiveRecord::FixtureSet.identify(:george) %>
YAML defaultsのサポート
fixtureのYAMLファイルでデフォルト値を設定して再利用できます。これはdatabase.ymlでデフォルトを指定するのに使われる手法と同じです。
訳注: この場合DEFAULTSはすべて大文字にする必要があります。
DEFAULTS: &DEFAULTS
created_on: <%= 3.weeks.ago.to_formatted_s(:db) %>
first:
name: Smurf
<<: *DEFAULTS
second:
name: Fraggle
<<: *DEFAULTS
ラベルが「DEFAULTS
」のfixtureはすべて安全に無視されます。
「DEFAULTS
」を使う他に、_fixture
セクションにignore
を設定して、どのフィクスチャを無視するかを指定することも可能です。
# users.yml
_fixture:
ignore:
- base
# or use "ignore: base" when there is only one fixture that needs to be ignored.
base: &base
admin: false
introduction: "This is a default description"
admin:
<<: *base
admin: true
visitor:
<<: *base
In the above example, 'base' will be ignored when creating fixtures. This can be used for common attributes inheriting.
fixtureのモデルクラスを設定する
fixtureのモデルクラスをYAMLファイルで直接指定できます。これは、fixtureがテストの外部で読み込まれ、かつset_fixture_class
が利用できない場合(bin/rails db:fixtures:load
の実行時など)に有用です。
_fixture:
model_class: User
david:
name: David
ラベルが「_fixture
」のfixtureはすべて安全に無視されます。
概要
MITライセンスに基づいて翻訳・公開いたします。