RSpecでスタブ(Stub)を使う

こんにちは。seimiyajunです
横浜にあるコワーキングスペースでRuby on Railsのもくもく会を月に1回、2年半くらいやっています
今回は[タネマキ Advent Calendar 2018](https://adventar.org/calendars/3626)の20日目ということで書きます

[タネマキ Advent Calendar 2018](https://adventar.org/calendars/3626)自体は1ミリも技術系のアドベントカレンダーではないのですが気にせずいきたいと思います

# 概要
## 環境
Ruby 2.5.1
RSpec 3.8.1

## やりたいこと
- 架空のWebサービス microtaskhubをAPI経由で利用し、その結果をAPIで返却する
- 外部APIを使うメソッドにおいて、create, deleteを実装する
- RSpecのスタブを使ってテストをスムーズに行う

## 流れ
1. createメソッドとdeleteメソッドを書く
2. テストコードを書く

# 開発

## 1. createメソッドとdeleteメソッドを書く

えいや、とこんな感じで。
http_methodというメソッドにparamsを渡せばいい感じにやってくれる前提です

```ruby:microtaskhub.rb
class Microtaskhub

  def create(username)
    params = {
      method: 'post',
      url: 'https://microtaskhub.net',
      username: username
    }
    response = http_method(params)
    "#{response['header']} : #{response['body']}"
  end

  def destroy(useranme)
    params = {
      method: 'delete',
      url: 'https://microtaskhub.net',
      username: username
    }
    response = http_method(params)
    "#{response['header']} : #{response['body']}"
  end

  private

  def http_method(params)
    # 本来はNet::http等で処理を書く
    {'header' => 200, 'body' => 'Success!'}
  end
end
```

## 2. テストコードを書く

### Requestが成功した場合

これはcreateについてはそのままテストが書けそうです

```ruby:microtaskhub_spec.rb
RSpec.describe Microtaskhub do
  describe 'create' do
    context 'request is succeed' do
      it 'returns success message' do
        expect(Microtaskhub.new.create('test')).to eq '200 : Success!'
      end
    end
  end
end
```

これでも動作はしますが、問題点があります

- 毎回APIを叩くので速度が遅い （1つだけなら気にならないかもしれませんが、こういったテストが30、100と増えていくと処理時間が問題になります）
- テストを実行するたびにmicrotaskhubでのユーザー数がどんどん増えていく （ユーザー数に上限があったり、課金ポイントになっているような場合は問題になります）
- ユニークネームでないと登録できない場合、ランダムな名前でcreateする必要が出てくる （fixtureなどを使えば解決できますが、これは本質ではないですね）
- microtaskhub側でcreateできない場合、ユーザーを削除のテストができない （招待してメール承認のフローがある場合などはcreateしただけでは削除ができません）

ということでここはstub化しましょう

```ruby:microtaskhub_spec.rb
RSpec.describe Microtaskhub do
  describe 'create' do
    context 'request is succeed' do
      it 'returns success message' do
        microtaskhub = Microtaskhub.new
        allow(microtaskhub).to receive(:http_method) { {'header' => 200, 'body' => 'test is created'} }
        expect(microtaskhub.create('test')).to eq '200 : test is created'
      end
    end
  end
end
```

既存コードの`　http_method`を`allow(microtaskhub).to receive(:http_method)`という記述で上書きしています
処理内容は`{}`内です。
なお処理が複数行ある場合は下記のように`do〜end`で書くこともできます。

```ruby:stubをdo~endで書く場合
allow(microtaskhub).to receive(:http_method) do
 #処理内容をここに書く
end
```

なお`destroy`もこんな感じで書けます

```ruby:microtaskhub_spec.rb
  describe 'destroy' do
    context 'request is succeed' do
      it 'returns success message' do
        microtaskhub = Microtaskhub.new
        allow(microtaskhub).to receive(:http_method) { {'header' => 204, 'body' => 'No Content.'} }
        expect(microtaskhub.destroy('test')).to eq '204 : No Content.'
      end
    end
  end
```

超重要:実際に対象のサービスがどのような結果を返してくるか、については事前に確認しておくこと

### stubの活用例：例外

`timeout`を実際に意図的に発生させるのは無理なので、stubに例外を投げてもらいます。

``` ruby:microtaskhub_spec.rb
    context 'timeout' do
      it 'returns timeout message' do
        microtaskhub = Microtaskhub.new
        allow(microtaskhub).to receive(:http_method) { raise Timeout::Error.new }

        expect(microtaskhub.create('test')).to eq '408 : Request Timeout.'
      end
    end
```

プロダクトコード側では例外をキャッチするようにします
（ここで例外をキャッチするのはすごくいけていないので参考にしないようにお願いします）

```ruby:microtaskhub.rb
  def create(username)
    params = {
      method: 'post',
      url: 'https://microtaskhub.net',
      username: username
    }

    response = http_method(params)
    "#{response['header']} : #{response['body']}"

  rescue Timeout::Error
    '408 : Request Timeout.'
  end
```

ここまでのコードを[リポジトリ](https://github.com/seimiyajun/qiita_rspec_stub)にまとめてあります。


# 改めてタネマキRailsもくもく会
私が主催で[Ruby on Rails もくもく会＠タネマキ](https://tanemaki-rails.connpass.com/)を月1回開催しています。
初心者歓迎ですので、もしご興味がありましたら参加ください