[RSpec] WebMockで正規表現とArrayなクエリストリングを扱う

# はじめに
[WebMock](https://github.com/bblimke/webmock)は昔からある有名なモックライブラリですが、RSpecでちょっとした細かい検証を行おうとした際にいろいろ試したことを自分用メモとしてまとめます。
主に外部API等へのリクエストを想定した、HTTPリクエストに対する検証の方法を取り扱っています。

# 環境
Ruby : 2.7.1
RSpec : 3.9.0
webmock : 3.8.3

## テスト対象のコード
サンプルとして今回は、https://jsonplaceholder.typicode.com/ へリクエストします。

```ruby:sample.ruby
require 'net/http'

class Sample
  def request(params = {})
    URI.parse('https://jsonplaceholder.typicode.com/todos')
      .tap { |uri| uri.query = URI.encode_www_form(params) }
      .then { |uri| Net::HTTP.get_response(uri) }
      .then { |res| res.body if res.is_a?(Net::HTTPSuccess) }
  end
end
```

## 基本形
webmockの`stub_request` を使用して、指定のURLへのリクエストをmockにします。
また`expect` 時の検証には `a_request` を使うパターンで行っています。


```ruby:sample_spec.rb
context '基本形' do
  before do
    stub_request(:get, 'https://jsonplaceholder.typicode.com/todos').and_return(status: 200, body: 'hoge')
  end

  it '正しくリクエストされること' do
    expect(Sample.new.request).to eq 'hoge'
    expect(a_request(:get, 'https://jsonplaceholder.typicode.com/todos')).to have_been_made.once
  end
end

context '基本形その2 クエリストリングも検証' do
  before do
    stub_request(:get, 'https://jsonplaceholder.typicode.com/todos?userId=2').and_return(status: 200, body: 'hoge')
  end

  it '正しくリクエストされること' do
    expect(Sample.new.request(userId: 2)).to eq 'hoge'
    expect(
      a_request(:get, 'https://jsonplaceholder.typicode.com/todos').with(query: { userId: 2 })
    ).to have_been_made.once
  end
end
```
- `have_bee_made` はWebMockが用意しているRSpec用のマッチャーです。他にもいくつかのマッチャーが用意されています。
  - https://github.com/bblimke/webmock/blob/master/lib/webmock/rspec/matchers.rb
- `once` も同じくWebMockが用意しているマッチャーで、こちらもいろいろなバリエーションがあります。 
 - https://github.com/bblimke/webmock/blob/master/lib/webmock/rspec/matchers/webmock_matcher.rb
- `with` を使用することで明示的にクエリストリングを検証できます。
  - `with`を使用しなくとも`a_request(:get, 'https://jsonplaceholder.typicode.com/todos?userId=2')` というように愚直に書いてもOKです。
  - クエリストリングの他に`with`では、`body` `headers` `basic_auth` という値を使用した検証も可能です。


## 正規表現でワイルドカード的なやつを使いたい
例えば、リクエストするURLを`todos`から`todos/1`に変えたい場合や、もっと複数のクエリストリングを指定したりしてテストしたい場合があると思いますが、その場合、毎回リクエストに合わせて`stub_request`のURL文字列を変更しないとmockされません。それだと面倒なので、正規表現を使うことで対応します。


```ruby
stub_request(:get, /https:\/\/jsonplaceholder.typicode.com/).and_return(status: 200, body: 'hoge')
```
こうすることで、`https://jsonplaceholder.typicode.com/` ドメイン配下のリクエストはすべてmockされて、`a_request`で検証可能になります。

また、正規表現は`a_request` でも使用することが可能です。

```ruby
expect(a_request(:get, /https:\/\/jsonplaceholder.typicode.com/)).to have_been_made
```

ただし、以下のように`a_request` で`with`を使用したクエリストリングの検証はできません。

```ruby
# この書き方はNG
expect(a_request(:get, /https:\/\/jsonplaceholder.typicode.com/).with(query: { userId: 2)).to have_been_made
```


## Arrayなクエリストリングの検証方法
以下のような同一のキーを複数指定したクエリストリングの検証をしたい場合の方法を説明します。

### Railsデフォルトな場合
Railsなアプリケーションの場合、以下のように`[]`を付けるのがデフォルトです。(`[]`はエンコードされるので正確には`%5B%5D`)

```
?userId[]=1&userId[]=2&userId[]=3
```

以下のように`with` に`query` のhashで配列を指定すればOKです。railsっぽく書けます。

```ruby
expect(
  a_request(:get, 'https://jsonplaceholder.typicode.com/todos').with(query: { userID: [1, 2, 3] })
).to have_been_made
```

### Railsデフォルトじゃない場合
外部APIなどRails以外のアプリケーションの場合、以下のように`[]`を付けないパターンがほとんどです。

```
?userId=1&userId=2&userId=3
```

そういうときは、以下のように`:flat_array` というsymbolをWebMockに設定します。

```ruby
WebMock::Config.instance.query_values_notation = :flat_array
```

検証方法は以下のように`with` の`query` hashで値を文字列で指定します。

```ruby
expect(
  a_request(:get, 'https://jsonplaceholder.typicode.com/todos').with(query: 'userID=1&userID=2&userID=3')
).to have_been_made
```

文字列を直接書くのが嫌な場合は、`URI.encode_www_form` などを使用して文字列に変換すると良いでしょう。

```ruby
expect(
  a_request(:get, 'https://jsonplaceholder.typicode.com/todos').with(query: URI.encode_www_form(userId: [1, 2, 3]))
).to have_been_made
```

### `flat_array`を設定した場合は`with`でクエリストリングがソートされる
`WebMock::Config.instance.query_values_notation = :flat_array` を設定した場合ですが、withでクエリストリングを検証するときはキーを文字順でソートした値を渡す必要があります。

表題のテスト対象のコードで以下を実行するとクエリストリングは `userId=1&aa=1`とリクエストされますが、 `with`の`query`には並び替えた文字列`aa=1&userId=1`を渡さないとダメでした。

```ruby
WebMock::Config.instance.query_values_notation = :flat_array

Sample.new.request(userId: 1, aa: 1)

# OK
expect(
  a_request(:get, 'https://jsonplaceholder.typicode.com/todos').with(query: 'aa=1&userId=1')
).to have_been_made

# hashはOK
expect(
  a_request(:get, 'https://jsonplaceholder.typicode.com/todos').with(query: { userId: 1, aa: 1 })
).to have_been_made

# これはNG
expect(
  a_request(:get, 'https://jsonplaceholder.typicode.com/todos').with(query: 'userId=1&aa=1')
).to have_been_made
```

# 参考
https://github.com/bblimke/webmock