?= と or= を CoffeeScript 1.2.0 で調べてみる

CoffeeScriptRuby に似ているところがたくさんあり,Ruby プログラマにも比較的馴染みやすい言語だと思います.たとえば,変数を初期化するのに便利な Ruby||= に対して,CoffeeScript にも予想通り or= があります.しかし,CoffeeScript (というか JavaScript)では nullundefined だけでなく 0"" (空文字列)も false 扱いなので困ることがあります.そんなときのために,or= のほかに ?= が用意されています.?CoffeeScript の存在確認演算子で,undefinednull のときにのみ false になります.これで,0""true 扱いできます.すなわち,

undefined?   #=> false
null? #=> false
0? #=> true
''? #=> true

となります.ここで a or= ba ?= b とは,それぞれ,

a or a = b
a ? a = b

と同じだと思っておいて間違いないだろう,というのがこのエントリの結論です.

本文では,or=?= を含んだ式を CoffeeScript 1.2.0 でコンパイルしてみて JavaScript でどうなっているかを確認していきますが,時間が無い人は上の結論だけで十分です.

はじめに

Tumblr?= と or= の違いを理解して,より CoffeeScript っぽいコードを書く というテキストをポストしたら,


a ?= ba or= b は,)正確にはそれぞれ

  • a ? a = b
  • a || a = b

の略です。

とコメントをもらいました.実は元 Tumblr のほうに「a ?= ba = b unless a? の短縮形」とか,「a or= ba = a or b の短縮形」と書いていたんです.このコメントが無ければ,それ以上考えることは無かったので,ありがたいコメントでした.

コメントをもらって,何が正確か調べてみようと最初は考えたのですが,http://coffeescript.org/ に行っても厳密な言語リファレンスがあるわけでもなく,何か「正確」が書いてある文書も無さそうなので,CoffeeScript 1.2.0 がどのように JavaScriptコンパイルするかを調べてみることにしました.

?= を調べてみる.

まずは,?= から調べてみます.ためしに,?= を使った例を CoffeeScript で書いてみました.

# conditional_assignment-1.coffee

a1 ?= b1 # 1)
a2 = b2 unless a2? # 2)
a3 ? a3 = b3 # 3)

これを,コンパイルしてみます.

% coffee -v
CoffeeScript version 1.2.0
% coffee -bp conditional_assignments-1.coffee
var a2, a3;

if (typeof a1 === "undefined" || a1 === null) a1 = b1;

if (typeof a2 === "undefined" || a2 === null) a2 = b2;

if (typeof a3 !== "undefined" && a3 !== null) {
a3;
} else {
a3 = b3;
};

実行結果は例 1)〜3) で変わらないものの,JavaScript は例 1) a1 ?= b1 と例 2) a2 = b2 unless a2? が同じ結果になりました.しかし,これだけで a ?= ba = b unless a? と同じと言い切ってはいけません.なぜなら,a?false になるときの戻り値が a ?= ba = b unless a? とで異なるからです.次に,戻り値を考慮した例を書いてみます.

# conditional_assignment-2.coffee

c = (a4 ?= b4) # 4)
c = (a5 = b5 unless a5?) # 5)
c = (a6 ? a6 = b6) # 6)

これを,コンパイルすると先程と様子が違います.

% coffee -bp conditional_assignments-2.coffee 
var a5, a6, c;

c = (typeof a4 !== "undefined" && a4 !== null ? a4 : a4 = b4);

c = (typeof a5 === "undefined" || a5 === null ? a5 = b5 : void 0);

c = typeof a6 !== "undefined" && a6 !== null ? a6 : a6 = b6;

今度は例 4) と例 6) が同じ結果を返すようになりました.ただし,なぜ例 6) のほうに括弧が無いのかは謎です.話を戻すと,例 5) は a5? が設定済みのときに void 0 すなわち undefined を返すようになっています.これに対して,例 4) と例 6) は,undefined にはなりません.

まとめると,a ?= b は,生成される JavaScript だけを見ると a = b unless a? と同じになることがあるが,戻り値を考慮するなら a ? a = b と同じ結果になるということです.

or= を調べてみる

こちらのほうも調べてみます.まず,CoffeeScript の例をつくって,

# conditional_assignments-3.coffee

a1 or= b1 # 1)
a2 = a2 or b2 # 2)
a3 or a3 = b3 # 3)

c = (a4 or= b4) # 4)
c = (a5 = a5 or b5) # 5)
c = (a6 or a6 = b6) # 6)

コンパイルしてみます.

% coffee -bp conditional_assignments-3.coffee 
var a2, a3, a5, a6, c;

a1 || (a1 = b1);

a2 = a2 || b2;

a3 || (a3 = b3);

c = (a4 || (a4 = b4));

c = (a5 = a5 || b5);

c = a6 || (a6 = b6);

こちらも括弧の有無はあるものの,「a or= ba or a = b」と言って差し支えありません.a or= b では a が真のときには代入が発生しないのに対し,a = a or b では a が真でも代入が発生するのが違います.

おわりに

CoffeeScript で便利な ?=or= について調べてみました.コンパイル後の JavaScript に若干の違いがありますが,それぞれ,a ? a = ba or a = b だと思っておいて間違いありません.a ?= ba = b unless a? とは戻り値が異ります.a or= ba = a or b は代入が発生するかどうかで違いがあります.以上,CoffeeScript 1.2.0 での話でした.

今回は Tumblr へのコメントで勉強させてもらいました.コメントをくださった id:murky-satyr に感謝します.

Shoulda で Rails のテストにコンテキストを導入する

Ruby on Rails (以下,Rails)標準のテスティングフレームワークである Test::Unit は,一般にはフラットにテストメソッドを定義するものだと思われています(たとえば,Rubyist Magazine - 改めて学ぶ RSpec).しかし,Test::Unit にコンテキストを導入して階層化するのは意外と簡単です.このエントリでは,Test::Unit にコンテキストを導入する方法として,ActiveSupport::TestCase にモンキーパッチを当てる方法と Shoulda を導入する方法を紹介します.さらに,一つ前のエントリで紹介した ActiveRecord モデルテストの例を Shoulda のコンテキストを使って書き直し,コードがコンパクトになることを示します.

RSpec の context に憧れて

前エントリの Rails の ActiveRecord モデルテストのガイドラインActiveRecord モデルテストを書いたときには,テストメソッドはフラットに並んでいました.参考までに gist を貼りつけておきます.

このテストケースを見ると,「実は ActiveRecord モデルの状態毎にテストメソッドをまとめられるのでは?」とか,「RSpec ならできるのに」とか考える人がいるかもしれません.いやいや,Test::Unit でも簡単に context を導入できるんですよ.まずは,原始的な方法から説明します.

ActiveSupport::TestCase にモンキーパッチ

まずは,こちらをご覧ください.

# test/test_helper.rb
class ActiveSupport::TestCase
def self.context(description = nil)
yield
end
end

何のことはない,ActiveSupport::TestCase のクラスメソッドとして context() を定義しただけです.context() が何をするかというと,何もしません.渡されたブロックを,ただ yield するだけです.しかし,これだけで,RSpec 風の context を Test::Unit で使えるようになります.最初に挙げたフラットなテストケースを context() を使って書き直したものを gist で貼り付けます↓

これは便利なので gem にして公開しようかとも思ったのですが,その必要はありませんでした.もっと便利な Shoulda を使いましょう.

Shoulda

Shoulda を使うと Rails のテストで RSpec のような context を定義できるようになります.Shoulda は gem として提供されているのでインストールは簡単です.Gemfile に

gem 'shoulda', groups: [:test]

として,bundle install すれば完了です.Shoulda を使ったテストの場合,コンテキスト内では test() の代わりに context() を使うことになっているので,モンキーパッチ版テストケースの test ブロックを should ブロックに置き換えます.置き換えたもの例によって gist で貼ります↓

Shoulda の context()/should() を使うと rake test の出力結果も変わります↓

ItemTest
test: with blank title should an item with blank title is invalid. PASS
test: with blank title should db can catch blank title. PASS
test: with boundary value attributes should an item with boundary value attributes is valid. PASS
test: with boundary value attributes should db allows saving an item with boundary value attributes. PASS
test: with empty price should an item with empty price is invalid. PASS
test: with empty price should db can catch null price. PASS
test: with empty title should an item with empty title is invalid. PASS
test: with empty title should db can catch null title. PASS
test: with negative price should an item with negative price is invalid. PASS
test: with negative price should db can catch negative price. PASS
test_db_can_catch_an_item_with_invalid_reference PASS
test_db_can_catch_null_vendor_id PASS

各テストメソッドが,test_ ではじまるテストメソッド名ではなく,"{context} should {should}" という文になっていることがわかります.この結果では,test_ テストメソッドのときの名前のまま放置していたので,おかしな文になっています.もう少し英語らしくなるように名前を修正してみます.gist ↓

これを rake test すると,

ItemTest
test: an item with blank title should an item with blank title is invalid. PASS
test: an item with blank title should be rejected by db. PASS
test: an item with boundary value attributes should be accepted by db. PASS
test: an item with boundary value attributes should be valid. PASS
test: an item with empty price should be invalid. PASS
test: an item with empty price should be rejected by db. PASS
test: an item with empty title should be invalid. PASS
test: an item with empty title should be rejected by db. PASS
test: an item with negative price should be invalid. PASS
test: an item with negative price should be rejected by db. PASS
test_db_can_catch_an_item_with_invalid_reference PASS
test_db_can_catch_null_vendor_id PASS

と少しはましになりました.

context 毎の setup

Rails の Test::Unit では,テストケース毎に 1 つの setup() をもち,この setup() は各テストメソッドで共通でした.Shoulda を使うと,context 毎に setup() を設定することができるようになります.前のテストケースでは,同一コンテキスト内で同じファクトリを何度も呼びだしているところがありました.これらはコンテキスト毎にsetup() でまとめられそうです.早速書き直してみたものを,またまたまた gist で貼りつけておきます↓

あれっ,かえって冗長になってしまいました.Shoulda のコンテキスト毎の setup() は役に立たないのでしょうか.実はこれ,間違ってはいないんですが私の考える理想的な setup() の使い方ではないんです.

context() + setup() の本当の実力

前の例ではコンテキスト毎に setup() を定義したためにかえってテストケースが長くなってしまいましたが,よくよく見てみると,ネガティブテストは同じパターンの繰り返しです.こんなとき,善良なる Rails 開発者なら,きっとこの gist のようにします.

これで,テストが短かく dry になりました.

おわりに

このエントリでは,Shoulda で Rails テストにコンテキストを導入する方法を紹介しました.前のエントリで紹介したフラットな Test::Unit テストケースを Shoulda で書き直し,context() とコンテキスト毎の setup() を導入することで,元のコードをコンパクトにすることがきました.実は,コンパクトにするだけなら Shoulda を使わなくても Test::Unit テストケースのままでもできるのですが,Test::Unit のときにはその気が起こらず,Shoulda のときには起きるので不思議です(Shoulda 使用者の主観で Shoulda の効用を裏づけるものではありません).

RSpec を使ってみたけど好きになれなかったという人は Shoulda を試してみてはいかがでしょうか.RSpec にあって Test::Unit には無い context() を補い,あなたのテストコードを小さく理解可能な状態に保つのに役立つはずです.

Rails の ActiveRecord モデルテストの書き方ガイドライン

このエントリでは,Ruby on Rails (以下 Rails)の ActiveRecord モデルテストについて,1) どこの何をテストすればよいか,2) どのようにテストを書けばよいか,のガイドラインを示します.このガイドラインRails 公式のものではなく,id:passingloop が使っている私的なものです.疑問・質問・批判・間違いの指摘はページ下部のコメント欄までお願いします.

はじめに

Rails は TDD/BDD サポートが充実した Web アプリケーション開発フレームワークです.Rails で使える Test::Unit や RSpec などといったテスティングフレームワークの使い方に関する解説も豊富にあります.しかし,「どこをどうテストすればよいのか」についての解説は,「使い方」の解説と比較して少ないように思います.もっとも,テスト一般についてどう書くかはアプリケーション次第というところもあるので,これは仕方ありません.とはいえ,ビジネスロジックを含まない「ActiveRecord モデル」なら,ある程度定型化できるのではないでしょうか.

ActiveRecord モデルのテストに関する情報源は多いものの,たくさんあり過ぎて全体像を把握しにくいのも問題です.思いつくだけでも,

があります.そこで,この文書では,私が収集したテストに関する情報をもとに,ActiveRecord モデルに対するテストの書き方ガイドラインを示すことを目標にします.

この文書では,まず,「ActiveRecord モデル」を定義します.この文書で扱う ActiveRecord モデル は MVC の 'M' ではないことに注意してください.次に,ActiveRecord モデルについて,どこの何をテストすればよいのか示します.最後に,Test::Unit を使ってテストをどう書けばよいのかを具体例で説明します.

ソースコードを埋め込むときに Gist を使っている箇所があります.JavaScript を無効にしていたり,https://gist.github.com へのリクエストを拒否していたりすると一部のソースコードが正しく表示されないことがあります.その場合は,直接 Gist を参照してください.

ActiveRecord モデルとは

ActiveRecord モデルとは,狭義には ActiveRecord::Base の派生クラスであり,app/models/model_name.rb ファイルで定義されるモデルクラスのことを指します.しかし,これだけでは,本文書の ActiveRecord モデル定義の半分です.残りの半分を定義するための材料として,Rails のモデルへの批判とそれに対する私の対応策について説明します.

Rails のモデルへの批判

人気 Web アプリケーションフレームワークRails は,その人気ゆえ,批判にさらされることも多くなります.その一例が Rails の モデルへの批判です.たとえば,Ruby on Railsの「えせMVC」の弊害 では,Rails のモデルが ドメインモデル貧血症 に陥りやすい設計であることを批判しています.実は,私もこの批判と同じ意見です.

貧血の治療法

しかし,ビジネスロジックをすべて ActiveRecord モデルに実装する必要があるかというと,必ずしも,そうではありません.たとえば,Dan Chak は著書 "Enterprise Rails" で物理モデルと論理モデルの分離を提案しています.Dan Chak の物理モデルはデータベースとテーブルの抽象化で,ActiveRecord で実現されるものです.物理モデルにビジネスロジックを含めてはいけません.一方,論理モデルはドメインモデルに相当し,ビジネスロジックは全てここに含めます.そして,コントローラなどクライアント向け API は論理モデルにだけ定義します.この方法の利点の一つめは,データベース設計に手を入れてもクライアント向け API が安定すること,二つめは,コントローラが容易にビジネスロジックを利用でき,データの整合性が保証されることです.

本文書の ActiveRecord モデル

私は,Dan Chak の方法とは少し違うやり方でドメインモデル貧血症を予防します.Dan Chak の実装では物理モデルと論理モデルを別々のクラスにしています.この方法では,物理モデルを app/models/physical に,論理モデルを app/models/logical に配置します.この方法は Railsディレクトリ構成規約からやや外れる方法で,物理モデルクラス名が ::Physical で修飾されるため不必要に長くなるという問題があります.そこで,私は,

  • ActiveRecord 派生クラスを物理モデルとして実装する
  • 論理モデルはモジュールとして実装する
  • 物理モデルに論理モデルモジュールを include して MVC の M を実現する

方法を採用しています.これにより,

を実現しています.

本文書の「ActiveRecord モデル」とは,ここで説明した物理モデルのことです.したがって,ActiveRecord モデルに対するテストは物理モデルに対するテストということになります.

どこをテストするのか

それでは,ActiveRecord モデルの,どこの何をテストすればよいのでしょうか.先に答えを言ってしまうと,どこをテストするのかについては

  • Validation
  • データベース

です.

Validation のテストについては,Ruby on Rails Guides が「validation をテストせよ」と書いています.Validation は ActiveRecord モデルクラスに定義されるもので,このクラスでしかテストできないものなので,ActiveRecord モデルでテストします.データベースに関するテストについては ActiveRecord モデルが物理モデルであることから,当然ここでテストします.

と書きましたが,本当に当然なのでしょうか? 次のデータベースに関するテストに関する議論で当然が正しかったかどうか確認します.

データベースに関するテストとは?

データベースに関するテストでは,

  • 制約テスト
    • CHECK
    • NOT NULL
    • UNIQUE
  • 参照整合性テスト
    • FOREIGN KEY

を行ないます.これらはデータベースのスキーマに関するテストなので,SqlUnit とか SchemaUnit とかというのを使ってデータベース側でテストすべきと考えるかもしれません.しかし,制約や参照整合性に違反するデータをデータベースに投入しようとした場合,ActiveRecord モデルは例外を投げることを思い出しましょう.そうなると,最早データベースから離れて ActiveRecord モデルの振舞いの問題なので,例外が期待通りに起こることを振舞いテストとしてテストする必要が出てきます.

つまり,データベースに関するテストとは,制約や参照整合性に違反したデータをデータベースに投入しようとしたときに発生する例外が,期待通りに発生することを確かめるネガティブテストだということになります.

他にもテストすべきものがあるのでは?

ここまで,ActiveRecord モデルでは validation とデータベースをテストすることを説明しましたが,他にもテストするべきものがあるのでは,と疑問に思う人がいるかもしれません.たとえば,

  • has_manybelongs_toのような関連定義
  • スコープ
  • その他のメソッド

です.しかし,これらは ActiveRecord モデルではテストしません.それはどうしてかというと,

has_many や belongs_to, スコープをテストしないのは?

has_many などやスコープといった ActiveRecord が提供するクラスメソッドは,Rails 本体でさんざんテストされているので,テスト無しに使っても大丈夫です.ただし,条件に lambda ブロックを使うなど,複雑なものはテストしてもいいかもしれません.でも,やっぱりしなくてもいいかもしれません.その理由は次を読んで考えてください.

その他のメソッドをテストしないのは?

ActiveRecord モデルクラスに定義されるメソッドは,インクルードされる論理モデルモジュールからのみ呼び出されるべきです.よって,これらのメソッドは基本的にはプライベートメソッドとして定義されます.ユニットテストでプライベートメソッドもテストすべきかどうかは議論が分かれるところですが,私は「プライベートメソッドまで無理してまでテストする必要はない.Ruby だからできるけど」という考えなので,テストしません.

プライベートメソッドをテストしなくても,それを呼び出す論理モデルモジュールのメソッドをテストするのですから,カバレッジ上は問題ありません.

Ruby on Rails Guides には違うことが書いてあるけど?

さんざん参考文献として例に挙げてきた Ruby on Rails Guides ですが,実は,ここで言っているのとは違うことが書いてあります.該当部分を引用して確認してみると,


3.3 What to Include in Your Unit Tests

Ideally, you would like to include a test for everything which could possibly break. It’s a good practice to have at least one test for each of your validations and at least one test for every method in your model.

と,本当は,

  • Validation
  • 全てのメソッド

をテストせよと書いてあります.この Ruby on Rails Guides とこの文書は矛盾しています.それでは,どちらが間違っているかというと,どちらも間違ってはいません.というのも,Ruby on Rails Guides とこの文書とではユニットテストの対象としているモデルの考え方が違うからです.

Ruby on Rails Guides が対象としているモデルがどのようなものかというと,...実は,はっきりと書いていません.ですので,validation とメソッドだけをテストすればいいようなケースも,きっと含んでいるのだと思います.一方,繰り返しですが,この文書のモデルは,より明確に定義された「ActiveRecord モデル」なので,validation とデータベースをテストします.

どのようにテストを書くか

ここまで,ActiveRecord モデルに対するテストは validation に関するテストとデータベースに関するテストの 2 つであることを説明してきました.それでは,これらの 2 つのテストをどのように書けばよいのでしょうか.これらのテストを書く際にはポジティブテスト(成功するテスト)とネガティブテスト(失敗するテスト)の 2 つを書きます.すなわち,

  • 正常値オブジェクト → エラーにならないこと
  • 異常値オブジェクト → エラーになること

をテストします.

ここでは,まず,ActiveRecord モデルの例として Item モデルを用意し,このモデルに対してテストを具体的に書いていきます.

例: Item モデル

何もないところにテストは書けないので,例として Item モデルを用意しました.

Item モデルと Vendor モデルとの関連図

Item モデルは,Vendor モデルに対して belongs_to 関連をもっています.この Item モデルのマイグレーションは次のようになります.

vendor_id, title, price の 3 つのカラムには NOT NULL 制約が設定されています.NOT NULL 制約は add_column メソッドで簡単に設定できます.さらに,11 行目から execute メソッドで SQL を実行しています.データベースには CHECK 制約と FOREIGN KEY 制約を追加したいのですが,Railsマイグレーションにはこの 2 つの制約を追加するメソッドが無いので仕方なく生 SQL に頼ります.余談ですが,この execute メソッドは,「簡単なことは簡単に,そうでないことはそれなりに」実現できる ActiveRecord の懐の深さを感じられるので,私が気に入っているメソッドの一つです.

話を元に戻して,Item モデルの ActiveRecord モデルは次のようになります.

belongs_to 関連と validation が設定されています.

データベースに制約があるのに同じ意味の validation を書くのはなぜか

Item モデルの title, price 2 つのカラムにはデータベース制約と validation の両方が設定されています.データベース制約があるのに同じ意味の validation を書くのは冗長だと思った人がいるかもしれません.しかし,validation が無いと,モデルの異常値でデータベースの例外がアプリケーションに飛んでしまいます.アプリケーションに不必要に例外を飛ばさないためにも,validation を設定しましょう.さらに,データベースの例外からデータの異常を探るのは難しいのですが,validation があるとアプリケーションで捕捉できるというメリットもあります.

実は,データベース制約と validation とでは検出する異常の意味合いが異なります.Validation で検出する異常は入力エラーです.ユーザにデータの修正を促すためのものです.一方,データベースで検出する異常はアプリケーションのバグです.開発者にコードの修正を促すためのものです.というわけで,私はデータベース制約と validation の両方を設定することにしています.

association_id である vendor_id のバリデーションを書かなかったのはなぜか

データベースの vendor_id カラムには NOT NULL 制約と FOREIGN KEY 制約がありました.しかし,ActiveRecord モデルの vendor_id には NOT NULL 制約に相当する validation はありませんでした.これは,vendor_id == nil は入力データの異常ではなく,アプリケーションのバグだと想定しているからです.すなわち,このアプリケーションは vendor_id をユーザが直接指定するような仕様ではなく,さらに,item.vendor = nil と書くようなユースケースも想定していないことを意味しています.そんなこと今までどこにも書いていませんでしたが,今書いたので許してください.

まとめると,NOT NULL/FOREIGN KEY 制約違反はバグというアプリケーションの仕様を想定しているので,validation で検出するよりデータベース例外を投げたほうがよいということです.

準備 1) スキーマRuby ではなく SQL で保存するよう Rails を設定する

テストを書いていく前の準備が 2 つあります.一つめは,スキーマRuby ではなく SQL で保存するよう Rails を設定することです.Rails が test 環境のデータベースを用意するときには,development 環境のデータベーススキーマをコピーします.このときに使うスキーマは,development 環境のデータベースをマイグレーションしたときに自動で保存されるスキーマで,デフォルトでは Ruby で書かれたマイグレーションコードとして保存されます.ここで問題となるのが,execute メソッドなどで追加した Rubyマイグレーションで表現できない SQL 文です.これらの SQL 文はデフォルトの Ruby 形式スキーマには保存されないので,test 環境のデータベースにコピーされません.このままでは制約をテストできません.

そこで,スキーマの保存形式を Ruby から SQL に変更します.config/applicaiton.rb に,

# config/application.rb
module Dummy
class Application < Rails::Application
config.active_record.schema_format = :sql
end
end

と, config.active_record.schema_format = :sql を追加しましょう.これで test 環境のデータベースにも制約がコピーされます.

準備 2) factory_girl を導入する

テストデータの作成に利用する factory_girl を導入します.Rails に factory_girl を導入する際は,factory_girl_rails gem が便利です.factory_girl_rails gem を導入するとジェネレータがフィクスチャリプレースメントとして factory_girl を使うようになります.Gemfile に

group :test do
gem 'factory_girl_rails'
end

を追加して,bundle update します.さらに,test/test_helper.rb で,

# test/test_helper.rb
class ActiveSupport::TestCase
include Factory::Syntax::Methods
end

と,ActiveSupport::TestCase クラスに Factory::Syntax::Methods モジュールをインクルードしておきましょう.このモジュールをインクルードすると,テストメソッド中で FactoryGirl.create(:foo) とする代わりに create(:foo) とスッキリ書けます.

なぜ Fixtures を使わないのか

Fixtures はデータベースの状態を用意します.だから,データベースに制約があるときには,制約に違反する異常値 fixture を作成したくでもできません.このため Fixtures は異常値テストには向きません.ActiveRecord モデルのテストでは,正常値のテストだけでなく異常値のテストも行なうため,フィクスチャリプレースメントとして factory_girl を採用しました.

だからといって,アプリケーションのテスト全体から Fixtures を追放するかというと,そういうわけではなく,論理モデルのテストでは Fixtures のほうが便利で,こちらを使うこともあります.つまり,Fixtures をフィクスチャリプレースメントで完全に置き換えるのではなく,いいとこ取りで併用します.併用する理由,すなわち,完全に置き換えない理由については factory_girl の採用面接 も合わせて読んでみてください.

Validation をテストする

ようやく準備が整いました.まずは validation のテストから書いていきます.

正常値オブジェクトがエラーにならないことを確認する

まず,ポジティブテストを書きます.すなわち,正常値オブジェクトがエラーにならないことを確認するテストです.このときに用意する正常値オブジェクトの属性値には,境界値を設定します.この境界値ファクトリの名前はそのまま,item_with_boundary_value_attributes という何のひねりもない名前にします.ファクトリ定義は,

# test/facytories/items.rb
FactoryGirl.define do
factory :item, aliases: [:item_with_boundary_value_attributes] do
vendor
title "Q"
description "Using this item, you will be taller."
price '0.00'
end
end

となります.このファクトリは Item クラスのオブジェクトを作るものなので,名前を :item として定義しておくとクラス名の設定が不要になり便利です.そして,本来つけたかった名前,:item_with_boundary_value_attributesエイリアスとして設定しています.

そして,いよいよテストメソッドです.テストメソッドは,"{ファクトリ名} is valid" とします.今回のファクトリ名は item_with_boundary_value_attributes だったので,

# test/units/item_test.rb
test 'an item with boundary value attributes is valid' do
assert build(:item_with_boundary_value_attributes).valid?
end

となります.build でデータベースに保存しないオブジェクトを作って valid? するだけです.簡単でしょ.

異常値オブジェクトがエラーになることを確認する

次にネガティブテストを書きます.すなわち,異常値オブジェクトがエラーになることを確認するテストです.異常値を用意する異常値ファクトリには,:{モデル名}_with_{異常な}_{属性名} という命名規則で名前をつけます.今回は,

  • :item_with_empty_title
  • :item_with_blank_title
  • :item_with_empty_price
  • :item_with_negative_price

の 4 つの異常値ファクトリを用意しました.このときに用意する異常値ファクトリは,ポジティブテストで用意した正常値(境界値)を少し修正するだけで用意できます.こんなときには factory_girl の継承機能が便利です.異常値ファクトリを追加して完成した Item モデルのファクトリ定義を次に示します.

次に,テストメソッドを書いていきます.テストメソッドは "{異常値ファクトリ名} is invalid" とします.今回用意したファクトリのそれぞれについてテストを書くと,

# test/units/item_test.rb
test 'an item with empty title is invalid' do
item = build(:item_with_empty_title)
assert item.invalid?
assert item.errors[:title].any?
end

test 'an item with blank title is invalid' do
item = build(:item_with_blank_title)
assert item.invalid?
assert item.errors[:title].any?
end

test 'an item with empty price is invalid' do
item = build(:item_with_empty_price)
assert item.invalid?
assert item.errors[:price].any?
end

test 'an item with negative price is invalid' do
item = build(:item_with_negative_price)
assert item.invalid?
assert item.errors[:price].any?
end

となります.各テストメソッドでは,異常値オブジェクトが invalid? であることと,異常値属性がエラーになっていることを確認します.

これで,validation のテストは完成です.

データベース制約・参照整合性をテストする

Validation のテストが片づいたところで,データベース制約・参照整合性テストに移ります.テストを書く前に,ActiveSupport::TestCase にデータベーステスト用のアサーションを追加しましょう.

準備: データベースエラーのためのアサーションを追加する

データベースに対するネガティブテストではデータベース例外の発生を確認します.具体的には,

  • ActiveRecord::StatementInvalid ... NOT NULL, CHECK 制約違反
  • ActiveRecord::RecordNotUnique ... UNIQUE 制約違反
  • ActiveRecord::InvalidForeignKey ... FOREIGN KEY 制約違反

が発生することを確認します.例外発生のアサーションには assert_raise が使えるのですが,毎回

assert_raise ActiveRecord::StatementInvalid do
...
end

assert_raise ActiveRecord::RecordNotUnique do
...
end

assert_raise ActiveRecord::InvalidForeignKey do
...
end

と長い例外名を書くのは面倒なので,専用のアサーションを用意します.それぞれ名前を,

  • assert_statement_invalid
  • assert_record_not_unique
  • assert_invalid_foreign_key

として,ActiveSupport::TestCase に追加しましょう.追加する場所は test/test_helper.rb が適当です.

# test/test_helper.rb
class ActiveSupport::TestCase
include Factory::Syntax::Methods

def assert_statement_invalid(&block)
assert_raise(ActiveRecord::StatementInvalid, &block)
end

def assert_record_not_unique(&block)
assert_raise(ActiveRecord::RecordNotUnique, &block)
end

def assert_invalid_foreign_key(&block)
assert_raise(ActiveRecord::InvalidForeignKey, &block)
end
end

これで準備が整いました.

正常値オブジェクトを保存できることを確認する

Validation のテストと同様,ポジティブテストから書いていきます.データベースのテストでは validation のテストで用意したファクトリを再利用します.:item_with_boundary_value_attributes ファクトリを使って,"db allows saving an item with boundary value attributes" テストメソッドを書きます.言い忘れましたが,データベースのポジティブテストでテストメソッドを書くときにはメソッド名を "db allows saving {正常値ファクトリ名}" とします.

test 'db allows saving an item with boundary value attributes' do
assert build(:item_with_boundary_value_attributes).save
end

制約をテストする

ここでも,validation のテストで用意した異常値ファクトリを全部そのまま再利用します.ただし,vendor_id カラムに NULL を設定するファクトリだけは用意していないので,build(:item, vendor_id: nil) として異常値オブジェクトを作ります.また,単に save! とするとデータベース制約よりも先に validation がエラーを検出してしまうので,{ :validate => false } として validation を回避します.テストメソッド名は,"db can catch {異常な状態の} {属性}" という命名規則で名前をつけます.そして,例外の捕捉には,準備しておいた制約違反用のアサーション assert_statement_invalid を使います.

test 'db can catch null title' do
assert_statement_invalid do
build(:item_with_empty_title).save!(validate: false)
end
end

test 'db can catch blank title' do
assert_statement_invalid do
build(:item_with_blank_title).save!(validate: false)
end
end

test 'db can catch null price' do
assert_statement_invalid do
build(:item_with_empty_price).save!(validate: false)
end
end

test 'db can catch negative price' do
assert_statement_invalid do
build(:item_with_negative_price).save!(validate: false)
end
end

test 'db can catch null vendor_id' do
assert_statement_invalid do
build(:item, vendor_id: nil).save!(validate: false)
end
end

参照整合性をテストする

参照整合性をテストするためのファクトリも validation のテストで用意していないので,正常値(境界値)オブジェクトの vendor_id を変更して,build(:item, vendor_id: 99999999) のようにして異常値オブジェクトを作成します.テストメソッド名は,"db can catch {モデル名} with invalid reference" とします.

test 'db can catch an item with invalid reference' do
assert_invalid_foreign_key do
build(:item, vendor_id: 99999999).save!(validate: false)
end
end

参照整合性テストで新たにファクトリを定義しなかったのはなぜか

参照整合性テストで異常値オブジェクトを作るときは新たにファクトリを定義せず,既存のファクトリを流用しました.:item_with_invalid_reference というファクトリを新しく作ってもよさそうなのに,と思った方がいたかもしれません.新たに定義しなかったのは理由は簡単で,ファクトリを定義したとしても一度しか呼び出されないからです.Validation のテストで用意したファクトリは,データベースのテストでも使われており,全て二回呼び出されています.制約テストで :item_with_null_vendor_id というファクトリを定義しなかったのも同様の理由です.

テスト完成

これで,テストを全て書き終えました.これまで書いたものを全て繋いで test/units/item_test.rb とします.

(参考)例で登場しなかったパターン: UNIQUE 制約のテスト

データベースの制約には三種類あります.

  • CHECK
  • FOREIGN KEY
  • UNIQUE

このうち,UNIQUE 制約は今回の例では登場しなかったので(参考)として補足します.UNIQUE 制約をもつモデルとして Foo モデルを別に用意しました.テストの説明に使えればいいので名前も中身もかなり適当ですが,必要十分です.まずは,マイグレーションから,

UNIQUE 制約は add_index メソッドで { :unique => true } することで設定できます.次に,ActiveRecord モデルクラスです.

UNIQUE 制約違反はバグではなく入力エラーなので validation を設定しています.

Validation のテスト

それでは UNIQUE 制約について validation のテストから書いていきましょう.テストメソッド名は "{モデル名} with duplicate {UNIQUE 属性} is invalid" とします.

# test/units/foo_test.rb
test 'a foo with a duplicate bar is invalid' do
create(:foo)
foo = build(:foo)
assert foo.invalid?
assert foo.errors[:bar].any?
end

3 行目の create(:foo) で作ったオブジェクトと同じ属性値をもつオブジェクトを 4 行目で作り,そのオブジェクトが invalid? であること(5 行目),bar 属性についてエラーとなること(6 行目)をアサーションしています.

データベースのテスト

次に UNIQUE 制約についてデータベースのテストを書きます.テストメソッド名は,"db can catch duplicate {UNIQUE 属性の属性名}" とします.

# test/units/foo_test.rb
test 'db can catch duplicate bars' do
create(:foo)
assert_record_not_unique do
build(:foo).save!(validate: false)
end
end

準備しておいた assert_record_not_unique アサーションを活用します.

おわりに

この文書では,ActiveRecord モデルテストの書き方についてガイドラインを示し,例を使って具体的に解説しました.最後に,この文書で登場したガイドラインをまとめておきます.

もう一つ,ファクトリ名やテストメソッド名の命名規則ガイドラインとして登場しました.こちらもまとめておきます.

  • 正常値ファクトリ名
    • :{モデル名}_with_boundary_value_attributes:{モデル名}エイリアス
  • 異常値ファクトリ名
    • :{モデル名}_with_{異常な}_{属性名}
  • Validation テストメソッド名
    • "{正常値ファクトリ名} with boundary value attribute is valid"
    • "{異常値ファクトリ名} is invalid"
  • データベーステストメソッド名
    • "db allows saving {正常値ファクトリ名}"
    • "db can catch {異常な} {属性名}"
    • "db can catch duplicate {UNIQUE 制約の属性名}"

大事なことなので二度言いますが,この文書は私的なものです.よって全ての Rails 開発に適用できるものではありません.ActiveRecord モデルの定義からも分かるように,このガイドラインエンタープライズアプリケーション開発を主対象としています.このガイドラインに忠実であるためには,データベース設計は「まとも」でないといけないですし,RDBMSSQL の「制約」をまともに扱えるものを採用する必要があります.データベースを NoSQL 的に使ったり NoSQL を使ったりする開発では何の役にも立ちません.そんな場合でも,どこの何をどのように,そして,それはなぜテストするのか考える上で参考になるかもしれません.もし何かの参考になれば幸いです.

それでは,世の中に ActiveRecord モデルテストが充実した Rails アプリケーションが増えますように.

このガイドラインを書いたときの Rails のバージョン

  • Rails 3.1.3
  • factory_girl 2.3.1
  • factory_girl_rails 1.4.0

factory_girl の採用面接: 自分の Rails プロジェクトで採用する前に確認すること

factory_girlRuby on Rails (以下 Rails)におけるフィクスチャリプレースメントの代表格で,採用している人も多いのではないでしょうか.「使い方」に関するブログ記事も多いです.しかし,採用を見送った人がその理由,「なぜ使わなかったのか」について書いている記事はごくわずかです.そこで,本エントリでは,factory_girl を Rails プロジェクトで採用できるかどうかを判断するための確認ポイントを説明していきます.


factory_girl: まだ Fixtures 使っているんですか.そんなのよりモダンなフィクスチャリプレースメントを使ったほうが,...

そんなことは,本エントリの対象外です.

面接開始

それでは,factory_girl の機能を確認していきましょう.

モジュールでネストしたクラス名に対応していますか?


factory_girl: Factory 定義が面倒になりますが,頑張ります.

factory_girl では,ファクトリ定義で定義したファクトリ名がクラス名になります.ですので,Inventory::Item モデルのファクトリを定義しようとして,

FactoryGirl.define do
factory :inventory_item
title "you will be taller"
vendor_id 1
end
end

としても,

Inventory::ItemTest
test_factory_girl_can_handle_a_nested_class_name ERROR
uninitialized constant InventoryItem
[...]

と,:inventory_itemInventory::Item ではなく InventoryItem を探しに行ってしまうのでエラーになります.これを避けるには,ファクトリ定義でモジュール名を含めたクラス名を指定する必要があります.

FactoryGirl.define do
factory :inventory_item, class: Inventory::Item do
title "you will be taller"
vendor_id 1
end
end

モジュールのネスティングが長くなると,ファクトリ定義が面倒なことになるのはマイナス評価です.しかし,モジュールのネスティングが長くなっているときには,モジュールに不必要に複雑な名前をつけていたり,モジュール階層設計でしくじっていたりする場合が多いので,それほど重大ではありません.

一方向関連 (association) に対応していますか?


factory_girl: ごめんなさい,関連を使うときには belongs_to も設定してください.

factory_girl は belongs_to で定義された Rails の関連を扱うことができます.たとえば,"User has_many posts" な 2 つのクラス,UserPost:

# app/models/user.rb
class User < ActiveRecord::Base
has_many :posts
end

# app/models/post.rb
class Post < ActiveRecord::Base
belongs_to :user
end

があるときに,post ファクトリで,

FactoryGirl.define do
factory :post do
title "hello factory_girl"
user # ← ここ重要
end
end

のように user 関連を定義しておいてから,Factory(:post)とすると,Post モデルのオブジェクトだけでなく,それに関連する User モデルのオブジェクトも同時に作られます.たとえば

irb(main):001:0> Factory(:post).user.email
=> "foo@example.com"

とっても便利ですね.

しかし,factory_girl は内部で Post#user= メソッドを使って関連づけを行っているため,belongs_to が設定されていないモデルでは関連機能を使えません.ためしに Post クラスの belongs_toコメントアウトして Factory(:post) すると,

irb(main):001:0> Factory(:post)
NoMethodError: undefined method `user=' for #<Post:0x00000103c66868>

と,怒られました.「モデルに不要な関連を設定しない」というポリシーで開発している私にとっては,belongs_to 必須というのは大きなマイナスポイントです.

複合キーに対応していますか?


factory_girl: composite_primary_keys プラグインと使ってもらえれば対応できます.

面接官: いや,それ複合キーが主キーでないと使えないし,...

先程の一方向関連で説明したように,factory_girl の関連機能は Railsbelongs_to に依存しています.belongs_to が複合キーに対応していない以上,factory_girl も対応できないのです.唯一の望みとしては,Dr. NicComposite Primary Keys プラグインがあるのですが,名前のとおり,複合キーが主キーでないと使えません.

このプラグインでもう一つ問題なのが Rails バージョンへの追従です.Composite Primary Keys プラグイン自体は素晴らしい出来なのですが,Rails の最新バージョンへの対応は迅速ではないようで,Rails 2→3 のときにしばらく待たされました.頻繁に機能追加し,Rail の最新バージョンを積極的に追いかけていくプロジェクトでは Composite Primary Keys プラグインを使うのは難しいので,この案は却下です.

しかし,複合キーに対応していないのは fixtures も一緒なので,この点では factory_girl をマイナス評価しないことにします.

最後に fixtures でできなくて,factory_girl にできることを一つ答えてください.


factory_girl: Fixtures はデータベースにレコードを作るのでデータベース制約に違反する fixture を定義できませんが,私は,FactoryGirl.build でデータベースに保存される前のオブジェクトを作ることができます.

面接官: ありがとうございました.

面接終了

ここまで factory_girl のアラ探しをしてきましたが,正直,よくできたフィクスチャリプレースメントなので,使えないシーンを考えるのに苦労しました.もし,採用することができれば,fixtures よりも短いコードでテストデータを用意することでできるので,このエントリで挙げたポイントに不満が無ければ fixture の代替として factory_girl を検討してみてはいかがでしょう.

私の意見

私の意見を述べさせてもらえれば,factory_girl で fixtures を完全には置き換えず,部分的に活用するのが良いと思います.すなわち,fixtures と factory_girl の併用です.というのも,

  • Fixtures はデータベースの状態を用意するもの
    • データベースに制約がある場合,制約に違反する fixture を作成できない
    • 異常値を検査するネガティブテストには使えない
  • factory_girl は ActiveRecord オブジェクトを用意するもの
    • ActiveRecord で容易に実現できないデータの状態を作成できない
    • Rails の規約から外れると,とたんに使い勝手が悪くなる

という違いがあるからです.

それでは,factory_girl が活躍できる場所はどこかというと,「fixtures でできなくて,factory_girl にできること」で述べた「データベースに保存される前のオブジェクトを作る」必要があるテストです.すなわち,異常値をもつオブジェクトを作成し,モデルのバリデーションやデータベース制約をテストを行う場面です.このような場面では,factory_girl の継承 (inheritance) 機能が活躍するのですが,それは別の機会にお話ししようと思います.

2011-11-25 追記:
factory_girl の継承機能が活躍する例を書いてみました→ Rails の ActiveRecord モデルテストの書き方ガイドライン - passingloopの日記

この記事を書いたときの Rails のバージョン

  • Rails 3.1.3
  • factory_girl 2.3.0
  • factory_girl_rails 1.4.0

Rails で元に戻せないマイグレーションを書くときの作法: ActiveRecord::IrreversibleMigration

ActiveRecordマイグレーションは,その中に up メソッドと down メソッドを書くことにより,データベースのスキーマを更新したり,変更を元に戻したりすることができ便利です.Rails 3.1 からは up, downchange メソッドにまとめて書くことができるようになりました.

しかし,すべてが元に戻せるマイグレーションかというと,そうではないことがあります.たとえば,

この図のように,integer だった vendor_code カラムを string に変更する場合,変更後のカラムに文字列を入れてしまうとマイグレーションで元に戻すことができません.文字列を数値に変換することができず,データが失われてしまうからです.このように,元に戻らないマイグレーションを書くときには,down メソッドで ActiveRecord::IrreversibleMigrationraise するようにします.

すなわち,マイグレーションは,

class ChangeVendorCodeToString < ActiveRecord::Migration
  def up
    change_column :items, :vendor_code, :string, null: false
  end

  def down
    raise ActiveRecord::IrreversibleMigration
  end
end

のようになります.こうしておくと,不用意な rollback でデータが破壊されるという悲劇がなくなります↓

% rake db:migrate
==  ChangeVendorCodeToString: migrating =======================================
-- change_column(:items, :vendor_code, :string, {:null=>false})
   -> 0.0295s
==  ChangeVendorCodeToString: migrated (0.0296s) ==============================
% rake db:rollback
==  ChangeVendorCodeToString: reverting =======================================
rake aborted!
An error has occurred, this and all later migrations canceled:

 ActiveRecord::IrreversibleMigration

 Tasks: TOP => db:rollback
 (See full trace by running task with --trace)

この記事を書いたときの Rails のバージョン

3.1.1

find_by_sql で派生カラムを作ると update_attributes で更新できてしまう

ActiveRecord::Base を継承したモデルクラスで,テーブルに存在しないカラムに対して #update_attributes すると ActiveRecord::UnknownAttributeError 例外が発生します.たとえば,

な foos テーブルに相当する class Foo < ActiveRecord::Base について,存在しないカラム bar を更新しようとすると怒られます.

irb(main):007:0> foo
=> #<Foo id: 1, title: "hogege", created_at: "2011-11-17 00:10:38", updated_at: "2011-11-17 00:10:38">
irb(main):008:0> foo.update_attributes(bar: 'hogege')
ActiveRecord::UnknownAttributeError: unknown attribute: bar
(以下略)

あたり前ですが unknown attribute: bar と言われています.

しかし,ActiveRecord のある機能を使っているときには, 存在しないカラムに対して #update_attributes を実行してもエラーになりません.たとえば,先程の foo に対して同じように #update_attributes を実行しているのですが,

irb(main):007:0> foo
=> #<Foo id: 1, title: "hogege", created_at: "2011-11-17 00:10:38", updated_at: "2011-11-17 00:10:38">
irb(main):008:0> foo.update_attributes(bar: 'hogege')
=> true

と,今度はエラーになりません.この二つ目の例は,最初の例と何が違うのでしょうか.

find_by_sql を使って派生カラムをつくる

実は,最初の例は Foo.find で作成したオブジェクト,二つ目の例は Foo.find_by_sql で作成したオブジェクトです.Foo.find で作成したオブジェクトのカラム属性は foos テーブルのカラム由来のものだけですが,Foo.find_by_sql を利用すると,SQL の JOIN を利用して foos テーブル以外のカラムも属性として利用できます.この,モデルクラスのテーブル以外に由来するカラムを派生カラム(derived column)*1と呼びます.

種明かしをすると,二つ目の例の foo は,

irb(main):001:0> foo = Foo.find_by_sql('select *, bars.title AS bar FROM foos, bars WHERE foos.id = bars.id').first
Foo Load (0.4ms) select *, bars.title AS bar FROM foos, bars WHERE foos.id = bars.id
=> #<Foo id: 1, title: "hogege", created_at: "2011-11-17 00:10:38", updated_at: "2011-11-17 00:10:38">

として find_by_sql で派生カラム bar を用意していたのです.

find_by_sql を使うときの注意点

注意しないといけないのは,派生カラムに対して #update_attributes などで更新してもデータベースのカラムは更新されないという点です.さらにやっかいなのは,カラムの値は変わらないのに,update_at は更新されるところです.ちょっとやってみましょう.

irb(main):002:0> foo.update_attributes(bar: 'hogegegegege')
(0.5ms) UPDATE "foos" SET "updated_at" = '2011-11-17 00:22:32.092490' WHERE "foos"."id" = 1
=> true
irb(main):003:0> Bar.find(1).title
Bar Load (0.3ms) SELECT "bars".* FROM "bars" WHERE "bars"."id" = ? LIMIT 1 [["id", 1]]
=> "hogege"

find_by_sql で作成した foo の派生カラムを #update_attributes で更新しようとしたけど実際には更新されない例なのですが,2 行目に表示されている SQL 文を見れば分かるように,bars テーブルは更新せずに foos テーブルの update_at だけを UPDATE しています.

分かっていれば何のことはないのですが,忘れていると


#update_attribute したのにカラムが更新されていない,更新したつもりがないのに update_at が書き変わっている

デバッグのときに小一時間悩むことになるので気をつけましょう.と,同時に,find_by_sql を利用するモデルクラスのメソッドは,ユニットテストで想定外の動作をしないか要確認です.

終わりに

本エントリでは、ActiveRecord の find_by_sql と update_attributes と update_at カラムとの素敵な関係についてお話ししました。find_by_sql なんて Rails の本線というか支線的なメソッドで、Rails 開発者でも多用する人は少ないと思います。しかし、SQL を駆使してデータベースを活用するときに便利なメソッドですので覚えておいて損はありません。私が好きな ActiveRecord のメソッドの一つです。本線を走れば高速に走れるけど支線を経由する自由を認めているのも Rails の魅力の一つですね。

どうでもいい話ですが、本線から外れるという意味で、やっとこのブログタイトルらしいエントリが書けました。

バージョン

この記事を書いたときの Rails のバージョンは 3.1.1 です。

*1:Agile Web Development with Rails 4th ed. の p.289 より

Rails の config で設定できる Rails 向け gem を作る

Rails アプリを開発していくと,プラグインやらエンジンやらで何やかんやと gem を作っていくことになります.このようにして作った gem は当然 Rails 向けなので,Rails アプリケーションの config で設定できるようにしたいところです.たとえば,passingloop という gem を作ったとして,この gem で token と email を設定できるとすれば,

Depot::Application.configure do
  config.passingloop.token = '15x3oegi'
  config.passingloop.email = 'passingloop@example.com'
end

のように書けるようにしたいところです.しかし,このように書くだけでは,

[...]/railties-3.1.1/lib/rails/railtie/configuration.rb:78:in `method_missing':
 undefined method `passingloop' for
 #<Rails::Application::Configuration:0x000001017db250> (NoMethodError)

と,config.passingloop が見つからずにエラーになります.そこで,泣く泣く

Depot::Application.configure do
  config.passingloop_token = '15x3oegi'
  config.passingloop_email = 'passingloop@example.com'
end

と "." を "_" に書き直して逃げたくなるのですが,格好悪いことこの上ありません.そこで,本エントリでは,gem ごとに config が名前空間をつくれるように,すなわち,はじめに紹介した

Depot::Application.configure do
  config.passingloop.token = '15x3oegi'
  config.passingloop.email = 'passingloop@example.com'
end

のように設定できる gem の作り方を解説します.

答は簡単 Rails::Railtie と ActiveSupport::OrderedOptions

先のエラーメッセージを見れば分かるように,

undefined method `passingloop' for
 #<Rails::Application::Configuration:0x000001017db250>
 (NoMethodError)

と,Rails::Application::Configuration なオブジェクトに passingloop が無いことが問題なので追加してあげます.ここでエラーメッセージにある Rails::Application::Configuration なオブジェクトの正体は,Rails::Railtie の config なので,gem で config にキー passingloop を追加してあげます.config.passingloop に追加するオブジェクトに Hash を使ってもいいのですが,

  • .name で name の値を取得できる
  • .name = value で name に値 value を設定できる

ようにしたいので,ActiveSupport::OrderedOptions を使います.この ActiveSupport::OrderedOptions は,Hash のように使えて,

hash['name'] = 'value'
hash['name']            #=> 'value'

と書く代わりに,

ordered_hash.name = 'value'
ordered_hash.name           #=> 'value'

と書ける優れものです.早速,gem に lib/passingloop/railtie.rb を作成し,

module Passingloop
  class Railtie < Rails::Railtie
    config.passingloop = ActiveSupport::OrderedOptions.new
  end
end

lib/passingloop.rb で忘れないように require して,

require "passingloop/version"
require "passingloop/railtie"

module Passingloop
...

rails c すると,

~/work/depot % rails c
Loading development environment (Rails 3.1.1)
irb(main):001:0> Depot::Application.configure do
irb(main):002:1*   puts config.passingloop.email
irb(main):003:1>   puts config.passingloop.token
irb(main):004:1> end
passingloop@example.com
15x3oegi
=> nil

今度はうまくいきました.

Engine と Plugin ならもっと簡単

Rails のエンジンとプラグインは,実は,両方とも Railtie です.ですので,たとえば,Rails エンジンを config で設定可能にするには,Railtie クラスをわざわざ作る必要はなく,Engine クラスの中で config に ActiveSupport::OrderedOptions を設定するだけで ok です.

module Blog
  class Engine < Rails::Engine
    isolate_namespace Blog

    config.blog = ActiveSupport::OrderedOptions.new
  end
end

これは,Blog エンジンの設定を Rails アプリケーションからできるようにする例です.

Railtie を使えば,config 以外にも Rails 的に設定できる.

本エントリでは,gem 独自の設定項目を Rails の config から設定できるようにする方法を説明しました.Railtie を使えば,config 以外にも initializer や generetor, middleware, Rake タスクなども gem から設定できるようになります.Gem を使わない Rails 開発ではあまり気にかけない Rails::Railtie ですが,一歩進んで gem 中心の開発を目指すのなら,一度ソースを読んでみることをお勧めします.