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


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

追記:
gemにした

* [rspec_compose_context | RubyGems.org | your community gem host](https://rubygems.org/gems/rspec_compose_context)
* [nanki/rspec_compose_context: Composable context for RSpec.](https://github.com/nanki/rspec_compose_context)

## 課題

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

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

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

## よくある風

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

* 本質的ではないネスト構造が邪魔でメンテナンス性が低い
* cに関しての記述が分散していて、c = 3とか増えた時に泣ける
* 可読性が低い

```ruby
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`の名前を書く必要がある。

```ruby
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以降では新しい方がデフォルトになっている)

* [shared contextにタグをつけて自動的にincludeする - Qiita](http://qiita.com/aereal/items/4c42f8e470ba6e061d40)
* [Consider changing the semantics of shared example group metadata · Issue #1790 · rspec/rspec-core](https://github.com/rspec/rspec-core/issues/1790)


## RSpec::Parameterizedを使う

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

* [tomykaira/rspec-parameterized: RSpec::Parameterized supports simple parameterized test syntax in rspec.](https://github.com/tomykaira/rspec-parameterized)

## module_evalを使う

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

[PICT](https://github.com/Microsoft/pict)とか使う機会がたまにあるが、相性は良さそう。
よくある風に書くと、結果に合わせてspecをいじるのはかなり辛いはず。


```ruby
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`の使い方はおかしいが下記のコードはこの点を解決する。


```ruby
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
```