オレオレルールを晒す第二弾。
ソースコード内のどの程度の割合を自動テスト出来るか、という指標を、テストカバレッジといいます。カバレッジの定義を真面目にやろうとするとC0とかC1とかいろいろ出てきますが、ここでは「とりあえず一通り動かす」という雑な理解で話を進めます。
ruby の弱点
よく言われる ruby の弱点の1つとして、「動的言語なので、他の言語ではコンパイル時にわかるエラーが事前に発見できない」というものがあります。
例えば以下のコードを御覧ください。
def edit
@post = Post.find(params[:id])
raise RestfulError::Forbiden unless @post.user_id == current_user.id
end
RSpec.describe 'edit' do
subject { get :edit, id: post.id, session }
let(:post){ FactoryGirl.create(:post, user_id: user.id) }
it { is_exptected.to render_template('edit') }
end
編集しようとする post が自分の post じゃなければ例外を投げる、という処理です。テストを雑に用意しましたが、このテストは raise RestfulError::Forbiden
の部分を通りません。
実は Forbiden
の部分でスペルミスをしているのですが、 ruby は動的言語ゆえ、実際にスペルミスの箇所を実行しない限り、エラーが起きません。 Java や C のようなコンパイル畑からやってきた方には「これだからrubyちゃんは信用できないのよね」と陰口を叩かれがちです。
そこで。
自動テストで「とりあえず全てのコード片を一通り評価させる」というのを目指すわけです。
(ちなみに RestfulError は拙作の gem です。ぜひご利用ください!)
条件分岐にそって context を書く
状況によって挙動が変わるメソッドのテストを書くときには、取りうる値を全部テストせよとか境界条件をテストせよとかこれもいろいろ理論があるのですが、とりあえず置いといて、ここではとにかく「カバレッジ100%を目指す」ことだけを考えます。
上述のコードを、条件分岐にそって context を書くと以下のようになります。
RSpec.describe 'edit' do
subject { get :edit, id: post.id, session }
let(:post){ FactoryGirl.create(:post, user_id: post_user_id) }
context '@post.user_id == current_user.id'
let(:post_user_id){ user.id }
it { is_exptected.to render_template('edit') }
end
context '@post.user_id != current_user.id'
let(:post_user_id){ 0 }
it { exptect{subject}.to raise_error }
end
end
(describe の次行に subject を、 context の次行に let を書く も参照ください)
もう少し複雑に条件分岐している例はこちら。条件分岐にそって context を書くようにすると、構造化のルールが一定し、100%カバーテストを記述しやすくなります。
def fizzbuzz(num)
if num % 3 == 0
ret = 'Fizz'
if num % 5 == 0
ret << 'Buzz'
end
elsif num % 5 == 0
ret = 'Buzz'
else
ret = num.to_s
end
ret
end
RSpec.describe 'fizzbuzz' do
subject { fizzbuzz(num) }
context 'num % 3 == 0' do
let(:num){ 3 }
it { is_expected.to eq 'Fizz' }
context '&& num % 5 == 0' do
let(:num){ 15 }
it { is_expected.to eq 'FizzBuzz' }
end
end
context 'num % 3 != 0 && num % 5 == 0' do
let(:num){ 5 }
it { is_expected.to eq 'Buzz' }
end
context 'num % 3 != 0 && num % 5 != 0' do
let(:num){ 4 }
it { is_expected.to eq '4' }
end
end