Rspecのshared_context基礎の基礎

調べようと思った最初のきっかけは、Rakeタスクのテストを調べていて見かけた[この記事](https://robots.thoughtbot.com/test-rake-tasks-like-a-boss)。

shared_context ??? な状況だったので、その場はそっ閉じ。

翌日、業務でのコードレビューでテストコードのリファクタを考えていたところ、再び shared_context をキーワードで見かけたので、今日のテーマとする。

## shared_contextの用途

名前のとおりですが、テストにおける条件(=context)を共有するために用いる

## 使い方

Rails 4.2.4
RSpec 3.3.0

### その1
- 人(Person)のモデルがあり、年齢を保持している。
- 20歳以上であれば、喫煙可能
- 20歳以上であれば、飲酒可能
- 20歳以上であれば、投票可能 ※選挙権引き下げられてるよ　というツッコミはなしで。。。

○ Person モデル

``` app/models/person.rb
class Person
  attr_reader :age

  def initialize(age)
    @age = age
  end

  def auth_smoke?
    @age >= 20
  end

  def auth_drink?
    @age >= 20
  end

  def auth_vote?
    @age >= 20
  end
end
```

#### ふつーに書いたRSpecコード

``` spec/model/person_spec.rb
require 'rails_helper'

describe Person do
  let(:person) { Person.new(age) }

  describe '#auth_smoke?' do
    context 'when age greater equals to 20' do
      let(:age) { 20 }
      subject { person.auth_smoke? }
      it { is_expected.to be_truthy }
    end
    context 'when age less than 20' do
      let(:age) { 19 }
      subject { person.auth_smoke? }
      it { is_expected.to be_falsy }
    end
  end

  describe '#auth_drink?' do
    context 'when age greater equals to 20' do
      let(:age) { 20 }
      subject { person.auth_drink? }
      it { is_expected.to be_truthy }
    end
    context 'when age less than 20' do
      let(:age) { 19 }
      subject { person.auth_drink? }
      it { is_expected.to be_falsy }
    end
  end

  describe '#auth_vote?' do
    context 'when age greater equals to 20' do
      let(:age) { 20 }
      subject { person.auth_vote? }
      it { is_expected.to be_truthy }
    end
    context 'when age less than 20' do
      let(:age) { 19 }
      subject { person.auth_vote? }
      it { is_expected.to be_falsy }
    end
  end
end
```

- let(:age) が各所に点在


#### shared_context を適用

``` spec/models/grade_table_spec.rb
require 'rails_helper'

shared_context 'infancy' do
  let(:age) { 19 }
end

shared_context 'adult' do
  let(:age) { 20 }
end

describe Person do
  let(:person) { Person.new(age) }

  describe '#auth_smoke?' do
    context 'when age greater equals to 20' do
      include_context 'adult'
      subject { person.auth_smoke? }
      it { is_expected.to be_truthy }
    end
    context 'when age less than 20' do
      include_context 'infancy'
      subject { person.auth_smoke? }
      it { is_expected.to be_falsy }
    end
  end

  describe '#auth_drink?' do
    context 'when age greater equals to 20' do
      include_context 'adult'
      subject { person.auth_drink? }
      it { is_expected.to be_truthy }
    end
    context 'when age less than 20' do
      include_context 'infancy'
      subject { person.auth_drink? }
      it { is_expected.to be_falsy }
    end
  end

  describe '#auth_vote?' do
    context 'when age greater equals to 20' do
      include_context 'adult'
      subject { person.auth_vote? }
      it { is_expected.to be_truthy }
    end
    context 'when age less than 20' do
      include_context 'infancy'
      subject { person.auth_vote? }
      it { is_expected.to be_falsy }
    end
  end
end
```

##### Pros
- let(:age) がまとめられた  
- 条件に 'adult(成人)' 'infancy(未成年)' と意味付けできた

##### Cons
- むしろ行数増えた
    - 今回は年齢だけだったけと、性別や配偶者有無といった要素が増えると効果出てきそう

### その2

shared_context は引数を取ることができるので、やってみた

- 各教科ごとに1〜5まで評価を持つ成績表(GradeTable)モデル
- 教科は、数学(math), 英語(english), 国語(japanese)とする

○GradeTableモデル

``` app/models/grade_table.rb
class GradeTable < ActiveRecord::Base
  validates :math,     numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 5 }
  validates :english,  numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 5 }
  validates :japanese, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 5 }
end 
```

#### ふつーに書いたRSpecコード

``` spec/models/grade_table_spec.rb
RSpec.describe GradeTable do

  describe 'validation' do
    let(:grade_table) { GradeTable.new(name: 'spec user', math: math, english: en, japanese: jp) }
    conetxt 'when gave valid value' do
      let(:math) { 3 }
      let(:en) { 3 }
      let(:jp) { 3 }
      it { is_expected.to be_valid }
    end
    describe 'math' do
      context 'when less than lower limit' do
        let(:math) { 0 }
        let(:en) { 3 }
        let(:jp) { 3 }
        subject { grade_table }
        it { is_expected.not_to be_valid }
      end
      context 'when equals to lower limit' do
        let(:math) { 1 }
        let(:en) { 3 }
        let(:jp) { 3 }
        subject { grade_table }
        it { is_expected.to be_valid }
      end
      context 'when greater than upper limit' do
        let(:math) { 6 }
        let(:en) { 3 }
        let(:jp) { 3 }
        subject { grade_table }
        it { is_expected.not_to be_valid }
      end
      context 'when equals to upper limit' do
        let(:math) { 5 }
        let(:en) { 3 }
        let(:jp) { 3 }
        subject { grade_table }
        it { is_expected.to be_valid }
      end
    end
    describe 'english' do
      context 'when less than lower limit' do
        let(:math) { 3 }
        let(:en) { 0 }
        let(:jp) { 3 }
        subject { grade_table }
        it { is_expected.not_to be_valid }
      end
      context 'when equals to lower limit' do
        let(:math) { 3 }
        let(:en) { 1 }
        let(:jp) { 3 }
        subject { grade_table }
        it { is_expected.to be_valid }
      end
      context 'when greater than upper limit' do
        let(:math) { 3 }
        let(:en) { 6 }
        let(:jp) { 3 }
        subject { grade_table }
        it { is_expected.not_to be_valid }
      end
      context 'when equals to upper limit' do
        let(:math) { 3 }
        let(:en) { 5 }
        let(:jp) { 3 }
        subject { grade_table }
        it { is_expected.to be_valid }
      end
    end
    ## 〜〜 japanese は省略 〜〜
  end
end
```

#### shared_context を使ってみる

``` spec/models/grade_table_spec.rb
RSpec.describe GradeTable do
  shared_context 'valid_data' do
    let(:math) { 3 }
    let(:en) { 3 }
    let(:jp) { 3 }
    subject { grade_table }
  end

  shared_context 'less_than_lower_limit' do |s|
    include_context 'valid_data'
    let(s) { 0 } if s
    subject { grade_table }
  end

  shared_context 'equals_to_lower_limit' do |s|
    include_context 'valid_data'
    let(s) { 1 } if s
    subject { grade_table }
  end

  shared_context 'equals_to_upper_limit' do |s|
    include_context 'valid_data'
    let(s) { 5 } if s
    subject { grade_table }
  end

  shared_context 'greater_than_upper_limit' do |s|
    include_context 'valid_data'
    let(s) { 6 } if s
    subject { grade_table }
  end

  describe 'validation' do
    let(:grade_table) { GradeTable.new(name: 'spec user', math: math, english: en, japanese: jp) }

    context 'when given valid value' do
      include_context 'valid_data'
      it { is_expected.to be_valid }
    end

    describe 'math' do
      context 'when less than lower limit' do
        include_context 'less_than_lower_limit', :math
        it { is_expected.not_to be_valid }
      end
      context 'when equals to lower limit' do
        include_context 'equals_to_lower_limit', :math
        it { is_expected.to be_valid }
      end
      context 'when greater than upper limit' do
        include_context 'greater_than_upper_limit', :math
        it { is_expected.not_to be_valid }
      end
      context 'when equals to upper limit' do
        include_context 'equals_to_upper_limit', :math
        it { is_expected.to be_valid }
      end
    end

    describe 'english' do
      context 'when less than lower limit' do
        include_context 'less_than_lower_limit', :en
        it { is_expected.not_to be_valid }
      end
      context 'when equals to lower limit' do
        include_context 'equals_to_lower_limit', :en
        it { is_expected.to be_valid }
      end
      context 'when greater than upper limit' do
        include_context 'greater_than_upper_limit', :en
        it { is_expected.not_to be_valid }
      end
      context 'when equals to upper limit' do
        include_context 'equals_to_upper_limit', :en
        it { is_expected.to be_valid }
      end
    end
    ## 〜〜 japanese は省略 〜〜
  end
end
```

##### Pros
- シンボルを引数にとることで、"境界値"というテスト観点をshared_contextにまとめることができた  

##### Cons
- shared_context の表す内容は、引数に頼ることになったので単体での具象度は下がった
- やっぱり行数は劇的には減っていない


## 所感
今回はコード量の削減にはならなかったけども、テストのパラメータをまとめられるのは強力だと感じた。

shared_context は、宣言した spec ファイルだけではなくて、require することで他のテストにも転用可能なので、project で共通の shared_context 用のファイルを作るのもありかと。

今回、subject も shared_context に突っ込んだけど、subject は it 内でテストコード書く側が意図的に宣言するのが良いのかなと思ってる。

引数もたせた例で、値も渡すことで shared_context を1つにまとめられるけど、context はある程度具体性を持たせたほうが良いと考えているので、あえてそうしてみた。

さぁて、shared_context の基礎もわかってきたので、[この記事](https://robots.thoughtbot.com/test-rake-tasks-like-a-boss)を読みなおしてみよう。

## 参考
- [改めて学ぶ RSpec](http://magazine.rubyist.net/?0035-RSpecInPractice)