ActiveRecord依存のModuleをrspecでテストする


Ruby on Railsにてモデルの機能を拡張する方法は、モジュールをインクルードするのが一般的です。インクルードするモジュールがActiveRecordに依存しない内容であれば、単体テストを書くことは難しくありません。しかし、ActiveRecordに依存している場合はちょっと面倒です。なぜなら、インクルードする側のモデルに対応するテーブルが必要になるからです。今回は、そんな血ヘドを吐くような困難に打ち勝つ方法をご紹介します。

環境

  • Ruby on Rails 4.0.0
  • rspec-rails 2.14.0

サンプルモデル・モジュール

まずは、インクルードする側のArticleモデルです。

📄db/migrate/001_create_articles.rb
class CreateArticles < ActiveRecord::Migration
  def change
    create_table :articles do |t|
      t.string  :title
      t.text    :content
      t.integer :updated_count, default: 0

      t.timestamps
    end
  end
end
📄app/models/article.rb
class Article < ActiveRecord::Base
  include UpdatedCountable
end

タイトル(title)・コンテント(content)・更新回数(updated_count)を持つモデルです。このモデルが UpdatedCountableモジュールをインクルードしています。

そして、こちらが UpdatedCountableモジュールです。

📄app/models/concerns/updated_countable.rb
module UpdatedCountable
  def self.included(base)
    base.class_eval do
      before_save :__updated_countable_up
    end
  end

  def __updated_countable_up
    self.updated_count += 1
  end
end

このモジュールをインクルードすると、自動的に before_saveコールバックにて updated_count属性がインクリメントされるという仕様になっています。トリガー的なモジュールというわけです。

それでは、この機能をrspecにてテストしましょう。(本当はテストが先なんですが!)

Articleモデルのspec

さっそく、spec/models/article_spec.rbに次のようなテストを書いてみました。

📄spec/models/article_spec.rb
require 'spec_helper'

describe Article do
  describe "updated_countable" do
    before { @article = Article.create! }
    it { expect { @article.save! }.to change(@article, :updated_count).by(1) }
  end
end

specを実行してみると・・・・・・見事に成功ですね! しかし、両手放しで喜んではいけません。もちろん、このままでも悪くはないのですが、少なくとも以下の点において非常に気持ちが悪いです。

  1. UpdatedCountableモジュールの仕様なのに、なんでArticleモデルのspecでテストしているんだ!
  2. 複数のモデルにインクルードされている場合、全部のモデルに同じspecを書くのか? 書かないのなら、どのモデルに書いたか忘れちゃいそう・・・
  3. 今後Articleモデルのテストコードが増えたときに、余計なコードがあると可読性が落ちる

といわけで、このテストをUpdatedCountableモジュール用に切り出してみましょう。

UpdatedCountableのspec

spec/models/concerns/updated_countable_spec.rbというファイルを作成し、そこにspecを書きます。

📄spec/models/concerns/updated_countable_spec.rb
require 'spec_helper'

describe UpdatedCountable do

  before :all do
    m = ActiveRecord::Migration.new
    m.verbose = false
    m.create_table :ars do |t|
      t.integer :updated_count, default: 0
    end
  end

  after :all do
    m = ActiveRecord::Migration.new
    m.verbose = false
    m.drop_table :ars
  end

  class Ar < ActiveRecord::Base
    include UpdatedCountable
  end

  describe "countup" do
    before { @ar = Ar.create! }
    it { expect { @ar.save! }.to change(@ar, :updated_count).by(1) }
  end
end

ポイントは before :all / after :all でダミーのテーブルを生成/破棄していることです。このテーブルが無いとArクラスはActiveRecordとして動いてくれません。specを実行してみると・・・・・・・・・成功です! う〜む、感慨深い。

しかし、まだちょっと気持ちが悪いですね。このままだと、この手のモジュールのspecを書く度に、テーブル生成・破棄の処理を書かなくてはいけません。明らかにリファクタリングの臭いを発するコードです。

こういったコードはspec_helperに持って行って、共通化してしましましょう。spec/spec_helper.rbの末尾に以下のコードを追加します。

📄spec/spec_helper.rb
...
...
def create_temp_table(name, &block)
  raise "no block given!" unless block_given?

  before :all do
    m = ActiveRecord::Migration.new
    m.verbose = false
    m.create_table name, &block
  end

  after :all do
    m = ActiveRecord::Migration.new
    m.verbose = false
    m.drop_table name
  end
end

呼び出す側の updated_countable_spec.rbも修正します。

📄spec/models/concerns/updated_countable_spec.rb
require 'spec_helper'

describe UpdatedCountable do

  create_temp_table :ars do |t|
    t.integer :updated_count, default: 0
  end

  class Ar < ActiveRecord::Base
    include UpdatedCountable
  end

  describe "countup" do
    before { @ar = Ar.create! }
    it { expect { @ar.save! }.to change(@ar, :updated_count).by(1) }
  end
end

非常にすっきりしました。再度specを実行すると・・・・・・・・・やっぱり成功! 見渡すかぎりの緑色。

これで、今後いくらモジュールが増えても怖くありません! なんだか、必要以上にモジュールを作ってしまいそうです。

関連する記事


「ActiveRecord依存のModuleをrspecでテストする」に3件のコメントがあります

  1. 趙自然

    I got this very strange question.
    Should it be
    Updatedcountable <– lowercase 'c' in countable
    instead of
    UpdatedCountable
    ?

    1. itmammoth

      I’m sorry to be not good at writing English, so it can be a strange module name for you.
      You can choose more appropriate name as you think.
      but how come you think using lowercase is better…?

      1. 趙自然

        Oh, I’m terribly sorry.
        I read it one more time.
        It is definitely my mistake.
        Your file name is app/models/concerns/updated_countable.rb (with the char _ that should work perfectly.

コメントは受け付けていません。