調べようと思った最初のきっかけは、Rakeタスクのテストを調べていて見かけたこの記事。
shared_context ??? な状況だったので、その場はそっ閉じ。
翌日、業務でのコードレビューでテストコードのリファクタを考えていたところ、再び shared_context をキーワードで見かけたので、今日のテーマとする。
shared_contextの用途
名前のとおりですが、テストにおける条件(=context)を共有するために用いる
使い方
Rails 4.2.4
RSpec 3.3.0
その1
- 人(Person)のモデルがあり、年齢を保持している。
- 20歳以上であれば、喫煙可能
- 20歳以上であれば、飲酒可能
- 20歳以上であれば、投票可能 ※選挙権引き下げられてるよ というツッコミはなしで。。。
○ Person モデル
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コード
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 を適用
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モデル
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コード
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 を使ってみる
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 の基礎もわかってきたので、この記事を読みなおしてみよう。