LoginSignup
6
4

More than 5 years have passed since last update.

RSpecの複数のcontextを組み合わせる方法を考える

Last updated at Posted at 2017-06-02

概要

複数のcontextを組み合わせる方法を思いついた

追記:
gemにした

課題

Webアプリでちょっと複雑なフォームのspecを書くとする。

かなり単純化した例を考える。

  • 項目aは1, 2の値を取りうる
  • 項目bは1, 2の値を取りうる
  • ただし、aが1の時のみ、bの値を入力することができる
  • それとは関係なく、cは1, 2の値を取りうる

よくある風

全6通りのパターンをよくある風に書き下すと下のような感じになる。

  • 本質的ではないネスト構造が邪魔でメンテナンス性が低い
  • cに関しての記述が分散していて、c = 3とか増えた時に泣ける
  • 可読性が低い
RSpec.describe "fukuzatsu na form" do
  context "a = 1" do
    subject do
      a + b + c
    end

    let :a do
      1
    end

    context "b = 1" do
      let :b do
        1
      end

      context "c = 1" do
        let :c do
          1
        end

        it { is_expected.to eq 3 }
      end

      context "c = 2" do
        let :c do
          2
        end
        it { is_expected.to eq 4 }
      end
    end

    context "b = 2" do
      let :b do
        2
      end

      context "c = 1" do
        let :c do
          1
        end
        it { is_expected.to eq 4 }
      end

      context "c = 2" do
        let :c do
          2
        end
        it { is_expected.to eq 5 }
      end
    end
  end

  context "a = 2" do
    subject do
      a + c
    end

    let :a do
      2
    end

    context "c = 1" do
      let :c do
        1
      end

      it { is_expected.to eq 3 }
    end

    context "c = 2" do
      let :c do
        2
      end
      it { is_expected.to eq 4 }
    end
  end
end

shared contextを使う

  • 悪くはない
  • specがfailした時に、include_contextしたcontextの名前は表示されないので、自分でcontextの名前を書く必要がある。
RSpec.describe "fukuzatsu na form" do
  shared_context 'a' do |value|
    let :a do
      value
    end
  end

  shared_context 'b' do |value|
    let :b do
      value
    end
  end

  shared_context 'c' do |value|
    let :c do
      value
    end
  end

  context do
    subject do
      a + b + c
    end

    context "a = 1, b = 1, c = 1" do
      include_context 'a', 1
      include_context 'b', 1
      include_context 'c', 1
      it { is_expected.to eq 3 }
    end

    context "a = 1, b = 1, c = 2" do
      include_context 'a', 1
      include_context 'b', 1
      include_context 'c', 2
      it { is_expected.to eq 4 }
    end
  end

  # 略
end

shared context + tag

shared_contextのタグを使った書き方でいいのではと思ったが、将来的にはこの書式には別の意味が与えられるので今から使うのは控えたい。(rspec-core3.5.0以降では新しい方がデフォルトになっている)

RSpec::Parameterizedを使う

contextではないが、パラメータの組み合わせで済む場合はrspec-parameterizedを使うと簡潔な表現で書けるらしい。
使われたり使われなかったりするパラメータがある場合はどうするのがいいんだろう。

module_evalを使う

  • contextはRubyのモジュールなので、module_evalで繋ぐことで動的にネストしている
  • 素直にrspecの構成要素を使っているので抵抗感が少ない
  • def ...はなんとかしたい

PICTとか使う機会がたまにあるが、相性は良さそう。
よくある風に書くと、結果に合わせてspecをいじるのはかなり辛いはず。

def a(value)
  proc do
    context "a = #{value}" do
      let :a do
        value
      end
    end
  end
end

def b(value)
  proc do
    context "b = #{value}" do
      let :b do
        value
      end
    end
  end
end

def c(value)
  proc do
    context "c = #{value}" do
      let :c do
        value
      end
    end
  end
end

def e(value)
  proc do
    it { is_expected.to eq value }
  end
end

def abc
  proc do
    context do
      subject do
        a + b + c
      end
    end
  end
end

def ac
  proc do
    context do
      subject do
        a + c
      end
    end
  end
end

RSpec.describe "fukuzatsu na form" do
  [
    [abc, a(1), b(1), c(1), e(3)],
    [abc, a(1), b(1), c(2), e(4)],
    [abc, a(1), b(2), c(1), e(4)],
    [abc, a(1), b(2), c(2), e(5)],
    [ac, a(2), c(1), e(3)],
    [ac, a(2), c(2), e(4)],
  ].map{|cs| cs.inject(self){|m, c| m.module_eval &c}}
end

まとめ

モジュールを動的にネストするのはRubyらしくて気に入っているが、そういったコードが好まれない環境もあると思われるので、shared_contextで頑張るのは割と良い気がする。

include_contextに渡すパラメータからcontextの名前も生成するようなメソッドを書けば、ほぼ同じことができるし、そちらの方がいいのかもしれない。

contextの名前は人間が読みやすいものをつける慣習があり、shared_contextの名前は参照しやすい名前(例えばエディタの補完機能と相性のよいsnake_caseとか)を使いたいとの想いがあるところが悲劇の始まりだと思った。
it_behaves_likeの使い方はおかしいが下記のコードはこの点を解決する。

RSpec.describe "fukuzatsu na form" do
  shared_context 'a' do |value|
    metadata[:description] = "a = #{value}"
    let :a do
      value
    end
  end

  shared_context 'c' do |value|
    metadata[:description] = "c = #{value}"
    let :c do
      value
    end
  end

  context do
    subject do
      a + c
    end

    it_behaves_like 'a', 1 do
      it_behaves_like 'c', 1 do
        it { is_expected.to eq 2 }
      end
    end
    # 略
  end
end
6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4