11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

くふうカンパニーAdvent Calendar 2019

Day 20

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

Last updated at Posted at 2019-12-19

くふうカンパニーアドベントカレンダー20日目の記事です。

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

新卒たちの質問

  • そもそもテストって何をテストするの
  • どこまでテストを書けばいいの
  • どこから書き始めてばいいの

そもそもテストって何をテストするの

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

どこまでテスト書けばいいの

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

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

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

どこから書き始めればいいの

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

テスト対象を書く

require 'rails_helper'

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

  # 他のメソッドがある場合は同じインデントで並べて書きます
  describe '他のメソッド' do
  end
end

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

あり得るテストケースを書き出す

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重くらいになってくるような場合はメソッドの設計を見直すことを考えます。
日本語で書くか英語で書くかは全体で決まっていればどちらでもいいと思います。

期待する結果を書き出す

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を使って期待する結果を書き出します。
ここまで書けると後からテストを見た時にメソッドがどのような振る舞いをするかわかるようになってきました。

テストコードを実装する

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を使って書いていきます。
ここまでできれば一旦テストとしては完成です。

テストのリファクタリング

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アプリケーションのテスト
ホワイトボックステストにおけるカバレッジ(C0/C1/C2/MCC)について
【初心者向け】テストコードの方針を考える(何をテストすべきか?どんなテストを書くべきか?)
RSpecえかきうた

11
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?