0
Help us understand the problem. What are the problem?

posted at

updated at

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

公式ドキュメントから

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

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

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

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

stub_data

Generates and returns stubbed response data from the named operation.

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

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.

特定のメソッドを呼び出した時に、返ってくるデータを設定します。
テストで使用するのはこちらが多いかと思います。

client = Aws::S3::Client.new(stub_responses: true)
client.list_buckets
#=> #<struct Aws::S3::Types::ListBucketsOutput buckets=[], owner=nil>

これにより、httpリクエストを実行することなく、レスポンスが返ってきます。
この場合は返ってくるデータを指定していないので、bucketsは空になっています。

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::ResoureAws::S3::Bucketなどのクラスを使用することも多いかと思います。

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

テストケース

今回は以下のような動作のテストを行う場合を想定します。

  1. Aws::S3::Bucketインスタンスのメソッドを使って、バケット内のS3オブジェクトのリストを取得し、特定の(sizeが0でない)オブジェクトを取得する
  2. オブジェクトの中身を取得する
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のソースを見てみます。

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を設定します

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を呼び出しているので、そのソースを確認します。

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に追加します。

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を設定することで、任意のデータを返却できるようになります。

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

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
0
Help us understand the problem. What are the problem?