53
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rspecのshared_context基礎の基礎

Last updated at Posted at 2015-09-20

調べようと思った最初のきっかけは、Rakeタスクのテストを調べていて見かけたこの記事

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 の基礎もわかってきたので、この記事を読みなおしてみよう。

参考

53
47
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
53
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?