5
1

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 3 years have passed since last update.

LIFULLAdvent Calendar 2019

Day 25

自分なりのテストコード(RSpec)の分類と、それぞれを書くときの注意事項

Last updated at Posted at 2019-12-25

はじめに

この記事はLIFULL Advent Calendar 2019の12/25分の投稿です。

メリークリスマス!
クリスマスにAdventCalenderを書くようになって早3年目です。
私はクリスマスと無縁のため、毎年せこせこ記事を書いています。リア充爆発しろ。

さて、弊社ではAPIの開発の際テストコードを書くのですが、毎回若手が悩み、相談される箇所があるため、今回はそれについてまとめようと思います。

もしかしたら弊社の開発フローや文化に依存しているかもしれませんので、違和感・誤りなどあればコメント頂ければ幸いです。

二行で

  • テストコードを大まかに分類すると、ユニットテスト / 機能テスト / E2Eの3つに分かれる
    • 上記、呼び方・表現は言語やフレームワークによって異なる
  • それぞれ目的に合わせて、モック・スタブを利用する

これだけです。
当たり前やん、という方も多いと思いますが、この記事はテストコード初学者向けとなりますのでご容赦ください。

私自身、初めてテストコード(RSpec)を書き始めたときにかなり迷った部分でもありますし、なかなか腹落ちできる記事にも出会えていないので、きっと誰かの役に立つはず。

テストコードの分類と注意事項まとめ

記事の中でそれぞれ説明しますが、このテーブルに書いてあることがすべてです。

分類 目的 注意事項
ユニットテスト 該当のメソッドの**ロジック(責務)**が正しいことを担保する 自身以外のメソッドに依存しない。すべてをモック・スタブする。
機能テスト 該当のクラス(のパブリックメソッド)の振る舞いが正しいことを担保する 自クラスのメソッドはモック・スタブしないこと。他のクラスはモック・スタブする。
E2Eテスト 該当のAPIまたはURL振る舞いが正しいことを担保する (可能な限り)なにもモック・スタブしない

それぞれサンプルコード(RSpec)を交えながら、具体的な注意事項に触れていきます。

ユニットテスト

目的は「該当のメソッドの**ロジック(責務)**が正しいことを担保する」です。
これを果たす上で最も重要な注意事項は、自身以外のメソッドに依存しないことです。

サンプルコードを見てみましょう。

sample1.rb
class Sample1
  def initialize(hoge:, fuga:)
    @hoge = hoge
    @fuga = fuga
  end

  def valid?
    return false unless hoge_valid?(hoge)
    fuga_valid?(fuga)
  end

  private
  def hoge_valid?
    !@hoge.nil? && @hoge.is_a?(Hash)
  end

  # fuga_valid?も同様に、なんらかロジックを持つ
end

この場合、valid?の責務は

  • hoge, fugaそれぞれがvalidかどうかを確かめること

です。
それぞれがどういうときにvalidかを知る必要はありません。

そのためvalid?のユニットテストでは、(hoge|fuga)_valid?をスタブします。

sample1_spec.rb
describe Sample1 do
  let(:target) { described_class.new(hoge, fuga) }
  let(:hoge) { double('hoge') }
  let(:fuga) { double('fuga') }

  describe '#valid?' do
    subject { target.valid?}

    let(:is_hoge_valid) { false }
    let(:is_fuga_valid) { false }

    before do
      allow(target).to receive(:hoge_valid?).and_return(is_hoge_valid)
      allow(target).to receive(:fuga_valid?).and_return(is_fuga_valid)
    end

    context 'when hoge is invalid' do
      it 'returns false' do
        is_expected.to eq false
      end
    end
    
    context 'when hoge is valid' do
      let(:is_hoge_valid) { true }

      context 'when fuga is invalid' do
        it 'returns false' do
          is_expected.to eq false
        end
      end

      context 'when fuga is valid' do
        let(:is_fuga_valid) { true }

        it 'returns true' do
          is_expected.to eq true
        end
      end
    end
  end

他のメソッドの実装ではなく、振る舞いに応じて自身の返り値を変更する、というvalid?の責務のみがテストできています。

こうすることで、例えばhoge_valid?のロジックが変更になり、nilのときはtrueを返すようになったとしても、valid?の実装もテストもなにも変えなくてよいようになります。

機能テスト

続いて機能テストです。こちらの目的は、
「該当のクラス(のパブリックメソッド)の振る舞いが正しいことを担保する」
でした。
機能テストの注意点は、「自クラスのメソッドはスタブしないこと」です。

先程のSample1クラスに少し手を加えて説明します。

sample2.rb
class Sample2
  def initialize(hoge:, fuga:)
    @hoge = hoge
    @fuga = fuga
  end

  def status
    return 'OK' if valid?
    'NG'
  end

  private
  def valid?
    return false unless hoge_valid?(hoge)
    fuga_valid?(fuga)
  end
end

statusというメソッドを追加してみました。

このSample2利用する側として知りたいことは、あるhoge, fugaを入れたときにstatusが何を返すか、だったりします。

実装上はvalid?がどういう判定であるか、なのですが、外からはそこは意識したくないですし、そういう内部処理を隠匿してこそそのクラスの振る舞いと言えると思います。

こういった要求をテストコードで実現したものが、機能テストになります。
イメージとしては、golangのTableDrivenTestsが非常に近いです。

samle2_spec.rb
describe Sample2 do
  let(:target) { described_class.new(hoge, fuga) }
  let(:hoge) { nil }
  let(:fuga) { nil }

  describe '#status' do
    subject { target.status }

    context 'when hoge and fuga are nil' do
      it 'returns NG' do
        is_expected.to eq 'NG'
      end
    end

    context 'when hoge and fuga are Hash' do
      let(:hoge) { {hoge: 'hoge'} }
      let(:fuga) { {fuga: 'fuga'} }

      it 'returns OK' do
        is_expected.to eq 'OK'
      end
    end
  end
end

付け加えると、場合にもよりますが、他クラスに依存している場合はスタブするようにしましょう。
他クラスへは「振る舞い」にしか依存するべきではありません。

次に行く前に

ここまでユニットテストと機能テストについて書きましたが、どうでしょう?
次のように思いませんでしたか?

「機能テストあればユニットテストいらなくね?」

そのとおりです。
基本的には、機能テストがしっかりと書けていればユニットテストは不要です。
なぜなら、そのクラスの責務はしっかりと担保されているから。

ただ、それなりに複雑なロジックを書き始めた際、
すべての入力パターンを機能テストで担保できますか?
本当に考慮漏れはないでしょうか?

なので、ユニットテストは機能テストを「補う」ためにあるものだと思ってください。

一つ一つのパーツがしっかりと作ってあり、ロジックが担保されている。
だから、一部代表的な機能がテストできていれば、そのクラスは信頼できる。

という立て付けで考えるといいと思います。

E2Eテスト

最後にE2Eテストです。こちらの目的は、
「該当のAPIまたはURL振る舞いが正しいことを担保する」
でした。

ユニットテスト、機能テストと広げて来たのでもはや自明かと思いますが、
E2Eテストの注意点は、「(可能な限り)なにもスタブしないこと」です。

あなたのサービスをユーザーが使うときの振る舞いをテストするものです。
外部API、DB、ロガー、あらゆるものが期待通りに動くかわかりません。
考えうる限りの入力で、APIやウェブページをテストしてください。

まぁ、実際は無理ですよね。
なので、先程書いたように、最重要(あるいは代表的)な部分をE2Eテストで担保して、それを下位のテスト(機能テスト、ユニットテスト)で補完しましょう。

最後に

当たり前のことですが、テストコードはプロダクトコードの品質を担保・維持するためのものです。
開発体制により、要求品質は異なるでしょうし、その方法も異なるでしょう。
大事なことは、それぞれのテストの目的をきちんと理解し、それに合わせた方法で書くことかと思います。

自分の考慮不足に気づけるので、私はユニットテストを書くのが大好きです。

5
1
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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?