AWS SDK Rubyでスタブを行う

## はじめに
AWS SDKを使用したコードに対してテストを記述したい場合、AWSのSDKで用意されているClientStubsを使用して、スタブを行うことが可能です。
ドキュメント自体は公式が出しているものがありますが、この例だけではやりたいこと（`Aws::S3::Client`以外のインスタンスを使うケース）が実現できなかったので、今回調べたことについてまとめました。

## 環境
- Ruby:       2.7.1
- aws-sdk:    3.1.0
- aws-s3-sdk: 1.114.0

## 公式ドキュメントから
https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ClientStubs.html

このドキュメントにあるように`Aws::ClientStubs`というモジュールが、各サービスに対応したClientクラスにincludeされています。

今回はS3を使って、その例を紹介します。

このモジュールに定義されているのは`api_requests`, `stub_data`,`stub_responses`の３つのインスタンスメソッドです。
基本的に使用するのは`stub_data`と`stub_responses`が多いと思うので、この2つについて説明します。

まずはドキュメント内の説明を簡単に抜き出します。

### stub_data
>Generates and returns stubbed response data from the named operation.

の通り、Clientから返ってくるレスポンスデータを生成します。

```ruby
s3 = Aws::S3::Client.new
s3.stub_data(:list_buckets)
#=> #<struct Aws::S3::Types::ListBucketsOutput buckets=[], owner=#<struct Aws::S3::Types::Owner display_name="DisplayName", id="ID">>
```

### stub_responses
>Configures what data / errors should be returned from the named operation when response stubbing is enabled.

特定のメソッドを呼び出した時に、返ってくるデータを設定します。
テストで使用するのはこちらが多いかと思います。
```ruby
client = Aws::S3::Client.new(stub_responses: true)
client.list_buckets
#=> #<struct Aws::S3::Types::ListBucketsOutput buckets=[], owner=nil>
```
これにより、httpリクエストを実行することなく、レスポンスが返ってきます。
この場合は返ってくるデータを指定していないので、bucketsは空になっています。

```ruby
client = Aws::S3::Client.new(stub_responses: {
  list_buckets: { buckets: [{name: 'my-bucket' }] },
  get_object: { body: 'data' },
})

# 以下のようにメソッドとして呼び出すことも可能です
client = Aws::S3::Client.new(stub_responses: true)
client.stub_responses(
  list_buckets: { buckets: [{name: 'my-bucket' }] },
  get_object: { body: 'data' },
)

client.list_buckets.buckets.map(&:name) #=> ['my-bucket']
client.get_object(bucket:'name', key:'key').body.read #=> 'data'
```
`stub_responses`を呼び出すことにより、返却されるデータを指定することができました。

## Aws::S3::Client以外のクラスを使う場合
ドキュメントに書いてある`stub_responses`の基本的な使い方は以上です。
しかし、実際にSDKを使用する場合には、`Aws::S3::Client`ではなく、`Aws::S3::Resoure`や`Aws::S3::Bucket`などのクラスを使用することも多いかと思います。

自分も今回そのようなケースをテストしようと思ったのですが、公式ドキュメントだけでは方法が分からず少し時間が掛かったので、そういった場合のスタブの設定方法を説明します。

### テストケース
今回は以下のような動作のテストを行う場合を想定します。
1. `Aws::S3::Bucket`インスタンスのメソッドを使って、バケット内のS3オブジェクトのリストを取得し、特定の（sizeが０でない）オブジェクトを取得する
2. オブジェクトの中身を取得する

```ruby
bucket = Aws::S3::Bucket.new('test-bucket')
objects = bucket.objects(prefix: 'test/')

has_content_object = objects.select{|object| object.size != 0 }
raise if has_content_object.empty?
# 1. この動作をテストする

objects.each |object|
  body = object.get.body
  CSV.parse(body)
  # 2. この動作をテストする
end
```

### bucketのオブジェクト一覧を取得する
まずは1のテストを行うため、`Aws::S3::Bucket#objects`のダミーを返せるようなスタブの方法です。
この場合、`Aws::S3::Bucket.new(stub_response: true)`ということはできません。スタブが可能なのはAws::S3::Clientのみです。
そこで`Aws::S3::Bucket#objects`のソースを見てみます。

https://github.com/aws/aws-sdk-ruby/blob/6889dcc88c49586233883d22c0e2a85d11045da5/gems/aws-sdk-s3/lib/aws-sdk-s3/bucket.rb#L909-L927
```ruby:bucket.rb
def objects(options = {})
  batches = Enumerator.new do |y|
    options = options.merge(bucket: @name)
    resp = @client.list_objects_v2(options)
    resp.each_page do |page|
      batch = []
      page.data.contents.each do |c|
        batch << ObjectSummary.new(
        bucket_name: @name,
        key: c.key,
        data: c,
        client: @client
        )
      end
      y.yield(batch)
    end
  end
  ObjectSummary::Collection.new(batches)
end
```


@client(=Aws::S3::Clientのインスタンス)の`#list_objects_v2`メソッドを呼び出していることがわかります。
そこで、`#list_object_v2`にstub_responsesを設定します

```ruby
client = Aws::S3::Client.new(stub_responses: true)
client.stub_responses(
  :list_objects_v2, {
    contents: [
      {key: 'test/my-directory-object', size: 0},
      {key: 'test/my-object', size: 100},
    ]
  },
)

bucket = Aws::S3::Bucket.new(name: 'test', client: client)
# スタブしたclientを使う
# 例えばrspecでテストする場合は、allow(Aws::S3::Client).to receive(:new).and_return(client)のように常にスタブしたclientを返すでも良いかもしれません

objects = Aws::S3::Bucket.new(name: 'test', client: client).objects(prefix: 'dummy')
objects.map{|object| [object.key, object.size]}
#=>　[["test/my-directory-object", 0], ["test/my-object", 100]]
```
このように、別のクラスのインスタンスで特定のデータを返すスタブを作成することができました。

### オブジェクトのbodyを取得する
取得したオブジェクトの中身を特定のデータにする、スタブの方法を確認します。
先ほど帰ってきたobjectのクラスを確認すると、`Aws::S3::ObjectSummary`となっています。
この中身を取得するために、`get`を呼び出しているので、そのソースを確認します。

https://github.com/aws/aws-sdk-ruby/blob/dc61dc24f8abc2942f370b06c403a2aff18d2bc0/gems/aws-sdk-s3/lib/aws-sdk-s3/object_summary.rb#L683-L690
```ruby:object_summary.rb
def get(options = {}, &block)
  options = options.merge(
  bucket: @bucket_name,
  key: @key
  )
  resp = @client.get_object(options, &block)
  resp.data
end
```
ここではclientの`get_object`を呼び出していることがわかりました。
そのため、これを先ほどと同様に`stub_responses`に追加します。
```ruby
csv = CSV.generate {|csv| csv << ['test', 'dummy']}

client.stub_responses(
  :get_object, {
   body: csv
  }
)

objects = Aws::S3::Bucket.new(name: 'test', client: client).objects(prefix: 'dummy')
objects.first.get.body.read
#=> "test,dummy\n"
```
これで任意のオブジェクトのデータを返却するスタブを作成することができました。
ただし、複数のobjectsに対してgetしても、全て同じデータが返ってくることになるので、複数ファイルの中身をテストする場合には使えないので、注意が必要です。

### 終わりに
このように、`Aws::S3::Client`以外のクラスを使う場合には、そのクラスのインスタンスのメソッド呼び出しを見て、最終的に`Aws::S3::Client`インスタンスが呼び出すメソッドを確認して、それに対して`stub_responses`を設定することで、任意のデータを返却できるようになります。

今回は正常なデータが返ってくる場合だけを書きましたが、例えば特定のエラーを返すようなスタブを作成することもできますので、公式ドキュメントを参照してみてください。