自分なりのテストの書き方 by RSpec

[くふうカンパニーアドベントカレンダー](https://qiita.com/advent-calendar/2019/kufu)20日目の記事です。

今年の新卒研修の中で自動テストの研修を担当してRSpecを使ったテストの書き方を説明しました。
しかしながら新卒エンジニア達からRSpecの使い方はわかったけれど、テストってどうやって書いていけばいいのと質問を受けたので私が普段どのようなステップを踏んでテストを書いているか振り返ってみることにしました。まだまだ不十分な研修で申し訳ない気持ちです😢
使用するテストフレームワークは実務で利用しているRSpecです。

# 新卒たちの質問
- そもそもテストって何をテストするの
- どこまでテストを書けばいいの
- どこから書き始めてばいいの


## そもそもテストって何をテストするの
作っている機能が動作することを確認(テスト)します。
例えば名前とメールアドレスを登録できる機能があったとします。ユーザ目線ではフォームから名前とメールアドレス入力して確定すると、登録されたというメッセージと登録情報が画面に表示される機能となります。そして内部構造に注目するとデータの有効性を検証しデータベースに保存されて登録完了画面にリダイレクトする機能になります。
これらの機能が仕様通り振る舞いが正しく動作しているかテストで確認するようにします。
ユーザ目線で仕様通り動作するか確認するテストはE2EテストとしてSystem specで、開発者目線で動作しているメソッドの振る舞いを確認するテストはユニットテストとしてModel specやController specで書いています。
今回はとりあえずModel specを例に書きました。

## どこまでテスト書けばいいの
どの程度の品質基準を目指すのかはソフトウェアテストの基準を参考にしています。

例としてUserクラスに以下のようなメソッドがあったとします。
ユニットテストに関してはホワイトボックステストの条件網羅(C2)を目指して書いています。下のようにシンプルな処理の時は分岐網羅(C1)で留めているものもあったりしますが。。

```ruby
def able_to_send_mail?
if name && email
  true
else
  false
end
```
条件網羅の時は、nameとemailの両方がtrue、emailのみfalse、nameのみfalseの3つのテストケースのテストを書くようにします。nameがfalseであればemailの真偽は判定に影響しないのでnameとemailがfalseのケースは省略しています。条件を書くか省くか迷った場合は書いておけば漏れはないので問題はありません。

||name|email|name.present?|email.present?|if文の判定|
|---|---|---|---|---|---|
|ケース1|'bob'|'bob @mail.com'|true|true|true|
|ケース2|'bob'|nil|true|false|false|
|ケース3|nil|'bob @mail.com'|false|true|false|

分岐網羅の時はこのようなコードがあった場合はif文がtureになるテストケースとfalseになるテストケースの2種類のテストを書くようにします。

||name|email|name.present?|email.present?|if文の判定|
|---|---|---|---|---|---|
|ケース1|'bob'|'bob @mail.com'|true|true|true|
|ケース2|nil|'bob @mail.com'|false|true|false|

## どこから書き始めればいいの
テストで網羅するテストケースはわかったので次は実際テストを順序立てて組み立てていきます。
テスト用のファイルが自動生成されていればそれを使って、なければ新しく作成します。

### テスト対象を書く
```ruby
require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#able_to_send_mail?' do
  end

  # 他のメソッドがある場合は同じインデントで並べて書きます
  describe '他のメソッド' do
  end
end
```
describeでテスト対象のメソッド名を書きます。
他にもテスト対象のメソッドが追加された場合は同じインデントでテストを追加していきます。

### あり得るテストケースを書き出す
```ruby
require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#able_to_send_mail?' do
    context 'nameとemailが存在する場合' do
    end
    context 'emailがnilの場合' do
    end
    context 'nameがnilの場合' do
    end
  end
end
```
contextで先ほど網羅したメソッドの実行する時にあり得るテストケースを書き出します。コンテキストをネストして書くこともできますが、ネストが3重くらいになってくるような場合はメソッドの設計を見直すことを考えます。
日本語で書くか英語で書くかは全体で決まっていればどちらでもいいと思います。

### 期待する結果を書き出す
```ruby
require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#able_to_send_mail?' do
    context 'nameとemailが存在する場合' do
      it 'trueを返すこと' do
      end
    end
    context 'emailがnilの場合' do
      it 'falseを返すこと' do
      do
    end
    context 'nameがnilの場合' do
      it 'falseを返すこと' do
      end
      # 他にも期待する結果がある場合は書きます
      it '他の期待値' 
      end
    end
  end
end
```
itを使って期待する結果を書き出します。
ここまで書けると後からテストを見た時にメソッドがどのような振る舞いをするかわかるようになってきました。

### テストコードを実装する
```ruby
require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#able_to_send_mail?' do
    context 'nameとemailが存在する場合' do
      it 'trueを返すこと' do
        user = User.create(name: 'bob', email: 'bob@mail.com')
        expect(user.able_to_send_mail?).to eq true
      end
    end
    context 'emailがnilの場合' do
      it 'falseを返すこと' do
        user = User.create(name: 'bob', email: nil)
        expect(user.able_to_send_mail?).to eq false
      do
    end
    context 'nameがnilの場合' do
      it 'falseを返すこと' do
        user = User.create(name: nil, email: 'bob@mail.com')
        expect(user.able_to_send_mail?).to eq false
      end
    end
  end
end
```
期待する結果を検証するためのコードをexpectを使って書いていきます。
ここまでできれば一旦テストとしては完成です。

### テストのリファクタリング
```ruby
require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#able_to_send_mail?' do
    subject { user.able_to_send_mail? }
    context 'nameとemailが存在する場合' do
      let(:user) { User.create(name: 'bob', email: 'bob@mail.com') }
      it { is_expected.to eq true }
      end
    end
    context 'emailがnilの場合' do
      let(:user) { User.create(name: 'bob', email: nil) }
      it { is_expected.to eq false }
    end
    context 'nameがnilの場合' do
      let(:user) { User.create(name: nil, email: 'bob@mail.com') }
      it { is_expected.to eq false }
    end
  end
end
```
重複を省いたり変数をletに書き出したりリファクタリングしました。
`expect(user.able_to_send_mail?)`はsubjectでまとめて、ローカル変数だった`user`はletを使うようにしました。期待する値も真偽値のみなので自然言語で書かれた期待値は削除しました。
もっとも過度にリファクタリングしてコードの重複を省いた結果、コードを追うことが難しくなるようであれば後からメソッドの振る舞いを知るのが難しくなってしまうので、ある程度の重複は許容してもいいと考えています。

# まとめ
今回は私がどのように考えながらテストコードを書いていっているか振り返ってみました。
今年のフィードバックを糧に今後の新卒たちの研修を改善していきたいと思います。

# 合わせて読みたい
[第6回　Webアプリケーションのテスト](https://gihyo.jp/dev/serial/01/tech_station/0006)
[ホワイトボックステストにおけるカバレッジ（C0/C1/C2/MCC）について](https://qiita.com/bremen/items/8b6542467d2a0066e5af)
[【初心者向け】テストコードの方針を考える（何をテストすべきか？どんなテストを書くべきか？）](https://qiita.com/jnchito/items/2a5d3e15761fd413657a)
[RSpecえかきうた](https://techracho.bpsinc.jp/takaneko/2018_12_13/66538)