はじめに
チームで開発している時にメンバーから
「Rspecよく分からーん」
という悩みの声があがりました。
確かに僕が初めてRspecに触れた時にも同じような感想を抱いた覚えたありました。
Rspecが何故分かりにくいのか説明し、じゃあどうしたら分かるようになるか?を解説していきます!
環境
今回の例をテストできる環境を用意しましたので手元で実際に試しながらやると理解が捗ると思います。
GitHubはこちら
DockerHubはこちら
実行手順
githubのREADMEに記載の通りです。
git clone https://github.com/yamamoto-hiroya/rspec_practice.git
cd rspec_practice/
docker-compose up -d
docker exec -it rspec_practice bash
// 以下docker内
cd /tmp
rspec sample1_spec.rb
↓以下のように実行できていれば成功です。
root@a6244fe37de9:/tmp# rspec sample1_spec.rb
.F
Failures:
1) sample1_test test false?????
Failure/Error: expect(true).to be false
expected false
got true
# ./sample1_spec.rb:11:in `block (3 levels) in <top (required)>'
Finished in 0.01019 seconds (files took 0.07753 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./sample1_spec.rb:10 # sample1_test test falseを返すこと
例
# coding: utf-8
describe 'sample2_test' do
# テストしたいメソッド
def get_pokemon_name(no)
pokemons = {1 => 'フシギダネ', 4 => 'ヒトカゲ', 7 => 'ゼニガメ'}
pokemons[no]
end
describe 'get_pokemon_name' do
subject { get_pokemon_name(no) }
shared_examples 'noに該当するポケモンの名前を返すテスト' do
it 'noに該当するポケモンの名前を返すこと' do
expect(subject).to eq my_expect
end
end
context '1の場合' do
let(:no) { 1 }
let(:my_expect) { 'フシギダネ' }
it_behaves_like 'noに該当するポケモンの名前を返すテスト'
end
context '4の場合' do
let(:no) { 4 }
let(:my_expect) { 'ヒトカゲ' }
it_behaves_like 'noに該当するポケモンの名前を返すテスト'
end
context '7の場合' do
let(:no) { 7 }
let(:my_expect) { 'ゼニガメ' }
it_behaves_like 'noに該当するポケモンの名前を返すテスト'
end
end
end
初心者であればこれを初見で見た時に「なるほどよく分からん」となるのではないでしょうか?
大丈夫です、私もそう思います。
何故分かりにくいか
これは色んな共通化を行った最終形を見せられているため分かりにくいのです。
具体的に見ていくと
subject
, shared_examples
, let
, it_behaves_like
このあたりがRspec特有の記法のため何をやっているか初見だと分かりにくい要因かなと思っています。
じゃあどうしたら分かるようになるか?
このテストが出来上がる過程を見ていけば分かりやすいです。
順を追って見ていきましょう。
第1歩目
# coding: utf-8
describe 'sample3_test' do
# テストしたいメソッド
def get_pokemon_name(no)
pokemons = {1 => 'フシギダネ', 4 => 'ヒトカゲ', 7 => 'ゼニガメ'}
pokemons[no]
end
describe 'get_pokemon_name' do
context '1の場合' do
it 'フシギダネを返すこと' do
result = get_pokemon_name 1
expect(result).to eq 'フシギダネ'
end
end
end
end
まず1ケース目を愚直に書きます。
この状態であれば
subject
, shared_examples
, let
, it_behaves_like
これら全て出てこないためRspecを知らなくてもユニットテストを書いたことがある人であればまぁ何となくは分かるかなと思います。
第2歩目
# coding: utf-8
describe 'sample4_test' do
# テストしたいメソッド
def get_pokemon_name(no)
pokemons = {1 => 'フシギダネ', 4 => 'ヒトカゲ', 7 => 'ゼニガメ'}
pokemons[no]
end
describe 'get_pokemon_name' do
context '1の場合' do
it 'フシギダネを返すこと' do
result = get_pokemon_name 1
expect(result).to eq 'フシギダネ'
end
end
context '4の場合' do
it 'ヒトカゲを返すこと' do
result = get_pokemon_name 4
expect(result).to eq 'ヒトカゲ'
end
end
end
end
2ケース目を愚直にコピペします。
ここまでも分かると思います。
第3歩目
# coding: utf-8
describe 'sample5_test' do
# テストしたいメソッド
def get_pokemon_name(no)
pokemons = {1 => 'フシギダネ', 4 => 'ヒトカゲ', 7 => 'ゼニガメ'}
pokemons[no]
end
describe 'get_pokemon_name' do
context '1の場合' do
let(:no) { 1 }
let(:my_expect) { 'フシギダネ' }
it 'フシギダネを返すこと' do
expect(get_pokemon_name(no)).to eq my_expect
end
end
context '4の場合' do
let(:no) { 4 }
let(:my_expect) { 'ヒトカゲ' }
it 'ヒトカゲを返すこと' do
expect(get_pokemon_name(no)).to eq my_expect
end
end
context '7の場合' do
let(:no) { 7 }
let(:my_expect) { 'ゼニガメ' }
it 'ゼニガメを返すこと' do
expect(get_pokemon_name(no)).to eq my_expect
end
end
end
end
ここで初めてletが出てきました。
そのテストケースで使う変数をletに定義しています。
第4歩目
# coding: utf-8
describe 'sample6_test' do
# テストしたいメソッド
def get_pokemon_name(no)
pokemons = {1 => 'フシギダネ', 4 => 'ヒトカゲ', 7 => 'ゼニガメ'}
pokemons[no]
end
describe 'get_pokemon_name' do
subject { get_pokemon_name(no) }
context '1の場合' do
let(:no) { 1 }
let(:my_expect) { 'フシギダネ' }
it 'フシギダネを返すこと' do
expect(subject).to eq my_expect
end
end
context '4の場合' do
let(:no) { 4 }
let(:my_expect) { 'ヒトカゲ' }
it 'ヒトカゲを返すこと' do
expect(subject).to eq my_expect
end
end
context '7の場合' do
let(:no) { 7 }
let(:my_expect) { 'ゼニガメ' }
it 'ゼニガメを返すこと' do
expect(subject).to eq my_expect
end
end
end
end
次にsubjectにしました。
今回の主題はget_pokemon_nameの返り値なのでこれをsubjectとしています。
第5歩目
# coding: utf-8
describe 'sample7_test' do
# テストしたいメソッド
def get_pokemon_name(no)
pokemons = {1 => 'フシギダネ', 4 => 'ヒトカゲ', 7 => 'ゼニガメ'}
pokemons[no]
end
describe 'get_pokemon_name' do
subject { get_pokemon_name(no) }
shared_examples 'noに該当するポケモンの名前を返すテスト' do
it 'noに該当するポケモンの名前を返すこと' do
expect(subject).to eq my_expect
end
end
context '1の場合' do
let(:no) { 1 }
let(:my_expect) { 'フシギダネ' }
it_behaves_like 'noに該当するポケモンの名前を返すテスト'
end
context '4の場合' do
let(:no) { 4 }
let(:my_expect) { 'ヒトカゲ' }
it_behaves_like 'noに該当するポケモンの名前を返すテスト'
end
context '7の場合' do
let(:no) { 7 }
let(:my_expect) { 'ゼニガメ' }
it_behaves_like 'noに該当するポケモンの名前を返すテスト'
end
end
end
最後にshared_examplesにテストをまとめました。
shared_examplesとit_behaves_likeはセットで使います。
これが1番最初に見せた状態になっています。
まとめ
今回の例で言うと5段階の共通化を経た状態が完成形となっているため、初見で見ると「なんじゃこりゃ?」となってしまうのではないかなというのが僕の考えです。
ただ共通化の過程が分かると少しは理解しやすいのではないかなと思います。
個人的にはテストコードの共通化はある程度で良いと思っています。
新しくプロジェクトにジョインした人が読めないよりは
ある程度読めてコピペでテストケースが追加できるような状態の方が優しい(易しい)んじゃないかなーと思います。
(これは個人的な感想で賛否あると思います。)
この記事で少しでもRspecが分かるようになったら嬉しいです!