RSpec の satisfy マッチャを便利に使う

Model のテストなどを書く時に次のように任意のリレーションに対して意図するクエリが追加されているかどうかをテストすることがあると思います。

class User < ActiveRecord::Base
  # rate 基準で上位10人を絞り込む
  scope :top10, -> { where(active: true).order(:rate).limit(10) }
end

RSpec.describe User do
  describe ".top10" do
    # scope top10 に対して意図するクエリが追加されていることをテストする
    subject { User.top10.to_sql }
    it { expect(subject.scan('WHERE "users"."active" = TRUE').one?).to be_truthy }
    it { expect(subject.scan('ORDER BY "users"."rate" ASC').one?).to be_truthy }
    it { expect(subject.scan('ASC LIMIT 10').one?).to be_truthy }
  end
end

テストとしては別に問題ないと思うんですが、 expect に負担がかかり過ぎてますね。
これを expect に負担をかけるのではなくてマッチャでがんばって書きたいと思います。

satisfy マッチャを使う

RSpec には satisfy マッチャがあります。
これは expect に渡した値を引数としたブロックを渡し、テストが成功しているか失敗しているかを判定することができるようになります。
例えば、次のように利用することができます。

describe "test" do
  # satisfy に渡したブロックが真を返せばテストがパスする
  # x = 1
  it { expect(1).to satisfy { |x| x.odd? } }
  # x = 2
  it { expect(2).to satisfy { |x| x.even? } }
end

先程の Model のテストは satisfy を利用すると次のように書くことができます。

RSpec.describe User do
  describe ".top10" do
    subject { User.top10.to_sql }
    # is_expected で書くことができる!
    it { is_expected.to satisfy { |sql| sql.scan('WHERE "users"."active" = TRUE').one? } }
    it { is_expected.to satisfy { |sql| sql.scan('ORDER BY "users"."rate" ASC').one? } }
    it { is_expected.to satisfy { |sql| sql.scan('ASC LIMIT 10').one? } }
  end
end

更にヘルパメソッドを定義することでオレオレマッチャみたいなのをさくっと定義することもできます。

RSpec.describe User do
  describe ".top10" do
    # let っぽく記述
    define_method(:scan_once) { |query| satisfy { |sql| sql.scan(query).one? } }
    subject { User.top10.to_sql }
    # 簡略化してかける
    it { is_expected.to scan_once 'WHERE "users"."active" = TRUE' }
    it { is_expected.to scan_once 'ORDER BY "users"."rate" ASC' }
    it { is_expected.to scan_once 'ASC LIMIT 10' }
  end
end

これは便利。