はじめに
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_data
とstub_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::Resoure
やAws::S3::Bucket
などのクラスを使用することも多いかと思います。
自分も今回そのようなケースをテストしようと思ったのですが、公式ドキュメントだけでは方法が分からず少し時間が掛かったので、そういった場合のスタブの設定方法を説明します。
テストケース
今回は以下のような動作のテストを行う場合を想定します。
-
Aws::S3::Bucket
インスタンスのメソッドを使って、バケット内のS3オブジェクトのリストを取得し、特定の(sizeが0でない)オブジェクトを取得する - オブジェクトの中身を取得する
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
のソースを見てみます。
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
を呼び出しているので、そのソースを確認します。
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
を設定することで、任意のデータを返却できるようになります。
今回は正常なデータが返ってくる場合だけを書きましたが、例えば特定のエラーを返すようなスタブを作成することもできますので、公式ドキュメントを参照してみてください。