MOE3: 定数定義の罠

OrderStatus の定数

テスト環境でちょっと嵌まった。

OrderStatus のクラス定義はこんな感じ。

class OrderStatus < ActiveRecord::Base
  UNEXECUTED = OrderStatus.where(code:  0).first
  INVALID    = OrderStatus.where(code:100).first
  SUCCESS    = OrderStatus.where(code:200).first
  SATISFIED  = OrderStatus.where(code:201).first
  FAILURE    = OrderStatus.where(code:300).first
  CUT        = OrderStatus.where(code:301).first
  STANDOFF   = OrderStatus.where(code:302).first
  DISLODGED  = OrderStatus.where(code:303).first
  CONFLICT   = OrderStatus.where(code:400).first
  DISBANDED  = OrderStatus.where(code:401).first
end

OrderStatus::SUCCESS のように使用する。code の値そのものには大して意味がないので気にしなくても良い。

重要なのは、OrderStatus クラスのロード時に db/seeds.rb で設定されたレコードが読み込まれて定数が定義されることだ。

落とし穴

開発環境では問題なく動く。

しかしテスト環境ではこれがうまくいかない。全てのステータス定数が nil になってしまうのだ。

ActiveRecord の遅延評価か、あるいはキャッシュが悪さしているのかとしばらく悩んで、ようやく原因に思い当たった。

テスト環境と db/seeds.rb

本来 Rails のテスト環境では db/seeds.rb は読み込まれないのだが、今回は OrderStatus のようにテスト環境でも使いたいマスタデータがあるので、spec/spec_helper.rb の冒頭で require している。

テスト環境では spec の example 実行の度に DB が初期化され、db/seeds.rb の再読み込みが行われてマスタデータが設定される。

# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'
require Rails.root.join("db", "seeds")

# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

#(略)

要するにタイミングの問題である。

db/seeds.rb によってマスタデータが生成される前に OrderStatus クラスがロードされているので、定数定義のためのクエリ実行時は order_status テーブルは空っぽなのだ。

定数定義の右辺、OrderStatus のクエリが常に nil を返すのは当然だった。

対策

本来のタイミングで定数が正しく定義されないのなら、テスト実行タイミングで定義しなおせばよい。

Ruby では定数の上書きは可能なのだが、それをやるといちいち警告が出て目障りなので spec/support/order_status.rb を下記の通り作成した。

このファイルは spec/spec_helper.rb によって自動的に読み込まれる。

# OrderStatus クラス再定義
Object.class_eval { remove_const :OrderStatus }
load Rails.root.join("app/models", "order_status.rb")

定数の再定義ではなく、OrderStatus そのものを再定義してしまうわけだ。

一応補足すると、require では app/models/order_status.rb の二度読みができないので load を使っている。

これでテスト環境でも OrderStatus が期待通りに機能するようになった。