最近、新人のテストコードを見る機会があり、ユニットテストの書き方について考える機会があった。ユニットテストはテンプレートみたいなものがあるので、それさえ押さえれば、誰でも簡単に書くことができる。
ここでは、その方法について紹介したい。サンプルはRSpecで書くが、その他のユニットテストフレームワークでも、応用ができるとおもう。
ごく単純化すると、テスト対象は状態を持ち、入力を与えると何らかの出力を行なうものである。入力が変われば出力は変化するし、状態が変化すると入力が同じでも出力が変わる(かもしれない)。
ユニットテストは、テスト対象の状態を操作し、与えた入力によって意図通りの出力を得られるかを確認する作業のことをいう。なので、ユニットテストを書くときには、オブジェクトの状態ごとにメソッド単位で入力と出力を確認するようにする。
RSpecの疑似コードで書くと、次のようなテンプレートになる。
例として、シンボルだけを受け付けるスタックを考えてみよう。このスタックは次のような仕様とする。
pushメソッドは、スタックの先頭にシンボルを追加する。
pushメソッドは、シンボル以外が渡されたときに例外を発生させる。
スタックが満杯のときに、pushメソッドは例外を発生させる。
スタックが空のときに、pushメソッドは例外を発生させる。
ユニットテストを書くときには、まず状態に注目する。すると、このスタックは空、満杯、それ以外という3つの状態を持ち、それぞれで振る舞いが異なることに気付く。そこで、まずはその状態をテストにコンテキストとして記述する。
次に、メソッド単位でテストを行なうため、状態ごとにメソッドをdescribeとして一覧する。
もし、入力によって出力が変化する場合には、その入力ごとにコンテクストを分ける。コンテクストは、同値分割や境界値分析を意識するとよい。
rspecコマンドのフォーマットオプションでドキュメンテーションフォーマットを指定して起動したときに、適切に読みくだせるのが理想的である。
% rspec -fd ~/Src/tmp/stack_spec.rb SymbolStack when stack is empty SymbolStack#size returns 0 (PENDING: Not yet implemented) SymbolStack#push with symbol increments stack size (PENDING: Not yet implemented) with non-symbol raise error (PENDING: Not yet implemented) SymbolStack#pop raise error (PENDING: Not yet implemented) when stack is full SymbolStack#size returns 10 (PENDING: Not yet implemented) SymbolStack#push with symbol increments stack size (PENDING: Not yet implemented) with non-symbol raise error (PENDING: Not yet implemented) SymbolStack#pop returns top (PENDING: Not yet implemented) when stack is not empty or full SymbolStack#size returns 1 (PENDING: Not yet implemented) SymbolStack#push with symbol increments stack size (PENDING: Not yet implemented) with non-symbol raise error (PENDING: Not yet implemented) SymbolStack#pop returns top (PENDING: Not yet implemented)
さて、ここから実際のテストの中身を書いていく。まずは、テスト対象の状態を作り出すための事前条件を書く。これは、コンテキストのテストを書くタイミングで書いてもいいし、最初にまとめて書いてもいい。
テストが通ることが確認できたら、テスト自体の可読性を高める工夫をしていこう。
it 'returns 0' do subject.size.should eq 0 end
は次のように書くことができる。(この書き方は、副作用がないときに限った方がよい)
its (:size) { should eq 10 }
また、下記のようにそのテストの本文がitの説明になっているときはitの引数を省略してもよいだろう。
it 'raise error' do expect { subject.push('data') }.to raise_error end
it { expect { subject.push('data') }.to raise_error }
これがやりすぎか、そうでないかは、rspecコマンドのフォーマットオプションでドキュメンテーションフォーマットを指定して確認するとよい。
SymbolStack when stack is empty SymbolStack#push with non-symbol should raise Exception
it { expect { subject.push(:data) }. to change { subject.size }.from(0).to(1) }
この場合、ドキュメンテーションフォーマットの出力は下記のようになる。
SymbolStack when stack is empty SymbolStack#push with symbol should change #result
「結果が変わる」とだけあって、どのようなテストであるかが分からなくなってしまっている。なので、この例はやりすぎだと分かる。
その他にもテストの可読性を高める工夫として、共通する部分をshared exampleにしたり、custom matcherをつくったりと、色々な書き方ができるので https://www.relishapp.com/rspec をざっと読んで、どのようなことができるのかを把握しておいたほうがよい。
このようにユニットテストは、最初に挙げた疑似コードのテンプレートを次の手順で書いていくことで機械的に書けるようになる。
メソッドごとに同値分割や境界値分析を意識して入力のコンテクストを分ける
この書き方に従ったテストコードの全体を下に載せるので、参考にしてほしい。