最近、新人のテストコードを見る機会があり、ユニットテストの書き方について考える機会があった。ユニットテストはテンプレートみたいなものがあるので、それさえ押さえれば、誰でも簡単に書くことができる。
ここでは、その方法について紹介したい。サンプルはRSpecで書くが、その他のユニットテストフレームワークでも、応用ができるとおもう。
はじめに
ごく単純化すると、テスト対象は状態を持ち、入力を与えると何らかの出力を行なうものである。入力が変われば出力は変化するし、状態が変化すると入力が同じでも出力が変わる(かもしれない)。
ユニットテストは、テスト対象の状態を操作し、与えた入力によって意図通りの出力を得られるかを確認する作業のことをいう。なので、ユニットテストを書くときには、オブジェクトの状態ごとにメソッド単位で入力と出力を確認するようにする。
RSpecの疑似コードで書くと、次のようなテンプレートになる。
describe 'テスト対象' do
context '状態' do
describe 'テスト対象メソッド' do
context '与える入力' do
it '期待する出力'
end
end
end
end
例として、シンボルだけを受け付けるスタックを考えてみよう。このスタックは次のような仕様とする。
- スタックの最大容量は10である。
push
メソッドは、スタックの先頭にシンボルを追加する。push
メソッドは、シンボル以外が渡されたときに例外を発生させる。- スタックが満杯のときに、
push
メソッドは例外を発生させる。 pop
メソッドはスタックの先頭のシンボルを返す。- スタックが空のときに、
pop
メソッドは例外を発生させる。 size
メソッドはスタックサイズを返す。
ユニットテストの書き方
ユニットテストを書くときには、まず状態に注目する。すると、このスタックは空、満杯、それ以外という3つの状態を持ち、それぞれで振る舞いが異なることに気付く。そこで、まずはその状態をテストにcontext
として記述する。状態によって振る舞いが変わらないオブジェクトであれば、状態ごとのcontext
は省略できる。
describe SymbolStack do
context 'when stack is empty' do
end
context 'when stack is full' do
end
context 'when stack is not empty nor full' do
end
end
次に、メソッド単位でテストを行なうため、状態ごとにメソッドをdescribe
として一覧する。
describe SymbolStack do
context 'when stack is empty' do
describe '#size' do
end
describe '#push' do
end
describe '#pop' do
end
end
context 'when stack is full' do
describe '#size' do
end
...
もし、入力によって出力が変化する場合には、その入力ごとにcontext
を分ける。context
は、正常系ばかりではなく異常系も含めるようにしよう。また、同値分割や境界値分析についても意識するとテストの質が上がる。
describe SymbolStack do
context 'when stack is empty' do
describe '#size' do
end
describe '#push' do
context 'with symbol' do
end
context 'with non-symbol' do
end
end
describe '#pop' do
end
...
それから、期待する出力を表明する。
describe SymbolStack do
context 'when stack is empty' do
describe '#size' do
it 'returns 0'
end
describe 'SymbolStack#push' do
context 'with symbol' do
it 'increments stack size'
end
context 'with non-symbol' do
it 'raises error'
end
end
describe 'SymbolStack#pop' do
it 'raises error'
end
...
rspecコマンドのフォーマットオプションでドキュメンテーションフォーマットを指定して起動したときに、適切に構造化されていることが重要である。
% rspec --color -fd symbol_stack_spec.rb
SymbolStack
when stack is empty
#size
returns 0 (PENDING: Not yet implemented)
#push
with symbol
increments stack size (PENDING: Not yet implemented)
with non-symbol
raise error (PENDING: Not yet implemented)
#pop
raise error (PENDING: Not yet implemented)
さて、ここから実際のテストの中身を書いていく。まずは、テスト対象の状態を作り出すための事前条件を書く。これは、コンテキストのテストを書くタイミングで書いてもいいし、最初にまとめて書いてもいい。
describe SymbolStack do
context 'when stack is empty' do
...
context 'when stack is full' do
before do
10.times {|i| subject.push(:"data#{i}") }
end
...
context 'when stack is not empty nor full' do
before do
subject.push(:data)
end
...
あとはひたすらテストの内容を書いていく。
describe SymbolStack do
context 'when stack is empty' do
describe '#size' do
it 'returns 0' do
expect(subject.size).to eq 0
end
end
describe '#push' do
context 'with symbol' do
it 'increments stack size' do
expect { subject.push(:data) }.
to change { subject.size }.from(0).to(1)
end
end
context 'with non-symbol' do
it 'raise error' do
expect { subject.push('data') }.to raise_error
end
end
end
...
テストが通ることが確認できたら、テスト自体の可読性を高める工夫をしていこう。
it 'returns 0' do
expect(subject.size).to eq 0
end
上記のようにit
の説明部分が、テスト部分とほぼ同等ということであれば、それを省略するのがよい。
it { expect(subject.size).to eq 0 }
subject
がコンテキストによって変わったりする場合であれば、適切な名前をつけることもできる。
describe SymbolStack do
...
context 'when stack is empty' do
subject(:empty_stack) { SymbolStack.new }
describe '#size' do
it { expect(empty_stack.size).to eq 0 }
end
その他にもテストの可読性を高める工夫として、共通する部分をshared exampleにしたり、custom matcherをつくったりと、色々な書き方ができるので https://www.relishapp.com/rspec をざっと読んで、どのようなことができるのかを把握しておいたほうがよい。
まとめ
ユニットテストは、最初に挙げた疑似コードのテンプレートを次の手順で書いていくことで機械的に書けるようになる。
- 状態をコンテキストとして分ける
- 状態ごとにメソッドを一覧する
- メソッドごとに入力のコンテキストを分ける
- 期待する出力を表明する
- 事前条件を書く
- テスト本体を書く
- テストをグリーンにする
- テストの可読性を高める
この手順で書いたテストは適切に構造化されるため、テストの意図が明確に伝わるテストになる。また、テストすべき条件に抜けや漏れがないかチェックもしやすい。この手順を活用して、適切で分かりやすいテストを書くように心がけよう。