RSpecによるユニットテストの書き方

最近、新人のテストコードを見る機会があり、ユニットテストの書き方について考える機会があった。ユニットテストはテンプレートみたいなものがあるので、それさえ押さえれば、誰でも簡単に書くことができる。

ここでは、その方法について紹介したい。サンプルは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 をざっと読んで、どのようなことができるのかを把握しておいたほうがよい。

まとめ

ユニットテストは、最初に挙げた疑似コードのテンプレートを次の手順で書いていくことで機械的に書けるようになる。

  • 状態をコンテキストとして分ける
  • 状態ごとにメソッドを一覧する
  • メソッドごとに入力のコンテキストを分ける
  • 期待する出力を表明する
  • 事前条件を書く
  • テスト本体を書く
  • テストをグリーンにする
  • テストの可読性を高める

この手順で書いたテストは適切に構造化されるため、テストの意図が明確に伝わるテストになる。また、テストすべき条件に抜けや漏れがないかチェックもしやすい。この手順を活用して、適切で分かりやすいテストを書くように心がけよう。