AWS SDK for Ruby V3のスタブを使ってみる

この記事は🎅GMOペパボエンジニア Advent Calendar 2022の14日目の記事です。

追記: Calendar for Ruby | Advent Calendar 2022 - Qiitaの14日目の記事としても公開しました。


RubyでS3を扱うとき、Class: Aws::S3::Client — AWS SDK for Ruby V3を使うことが多いだろう。 これを利用したアプリケーションのテストを書く場合、S3に実際にリクエストを飛ばすのは好ましくないので別の手段を取りたいところ。

実はAWS SDK for Ruby V3はスタブも提供している。 便利そうなので試しに使ってみた。

docs.aws.amazon.com

今回はS3を前提にしているが、EC2やLambdaのインスタンスでもスタブが使えるっぽい。

基本的な使い方

Aws::S3::Client.newインスタンスを生成する時に、stub_responses:を定義すればそのインスタンスで該当のAPIを実行すると定義したスタブがレスポンスとして返ってくる。 例えばGetObjectのスタブはこんなふうに定義できる。

client = Aws::S3::Client.new(
  stub_responses: {
    get_object: {
      body: 'data',
      etag: '"084ad94f7e17dcd8165b624b06d35eab"'
    }
  }
)

もしくはこう。

client = Aws::S3::Client.new(stub_responses: true)
client.stub_responses(
  :get_object, {
    body: 'data',
    etag: '"084ad94f7e17dcd8165b624b06d35eab"'
  }
)

スタブを定義したAws::S3::Clientインスタンスget_objectを実行すれば、定義したスタブ通りのレスポンスを取得できる。 ちなみにget_objectの引数に何を指定しても同じレスポンスが返ってくる。

res = client.get_object('path/to/key')
puts "body : #{res.body.read}"
puts "etag : #{res.etag}"
# 実行結果
body : data
etag : "084ad94f7e17dcd8165b624b06d35eab"

エラーをスタブしたい場合

エラーをスタブすることもできる。 やり方は https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ClientStubs.html#stub_responses-instance_methodStubbing Errorsに書かれている。

例えばクレデンシャルが誤っていると The AWS Access Key Id you provided does not exist in our records.というエラーが返されるが、これを再現したい場合はRuntimeErrorインスタンスとしてスタブに注入すればOK。

client = Aws::S3::Client.new(
  stub_responses: {
    get_object: RuntimeError.new("The AWS Access Key Id you provided does not exist in our records.")
  }
)
response = client.get_object(bucket: 'sample-bucket', key: 'path/to/key')
# 実行結果
RuntimeErrorが発生し、以下のメッセージが表示される

The AWS Access Key Id you provided does not exist in our records. (RuntimeError) 

複数のAPIについてスタブを定義したい場合

複数のスタブをまとめて定義することもできる。 例えば以下のようにGetObjectとListObjectsのスタブを定義できる。

client = Aws::S3::Client.new(
  stub_responses: {
    get_object: {
      body: 'data',
      etag: '"084ad94f7e17dcd8165b624b06d35eab"'
    },
    list_objects: {
      contents: [
        {
          key: 'sample_key1',
          etag: '"084ad94f7e17dcd8165b624b06d35eab"'
        },
        {
          key: 'sample_key2',
          etag: '"b9bca3e0a73bde48ee06bc37dfa74753"'
        }
      ]
    }
  }
)

puts "GetObject"
response = client.get_object(bucket: 'sample-bucket', key: 'path/to/key')
puts "body : #{response.body.read}"
puts "etag : #{response.etag}"

puts "ListObjects"
response = client.list_objects(bucket: 'sample-bucket')
response.contents.each{|c|
  puts "#{c.key}, #{c.etag}"
}
# 実行結果
GetObject
body : data
etag : "084ad94f7e17dcd8165b624b06d35eab"
ListObjects
sample_key1, "084ad94f7e17dcd8165b624b06d35eab"
sample_key2, "b9bca3e0a73bde48ee06bc37dfa74753"

スタブだと不十分な場合

PutObjectやCopyObjectなどS3のオブジェクトに更新がかかるようなAPIについては、スタブだと不十分かな。 一応スタブ化できるが、たとえばPutObjectだとContent-MD5を利用したオブジェクトの整合性チェックとかがされないまま、決めうちのETagやバージョンIDを返すだけになる。 これで十分といえる場面は結構限られそう(それをいったらGetObjectなどもスタブでは不十分な場面もありそうだが)。

スタブで不十分な場合は、MinIOを利用する方法もある。 MinIOとはgoで実装されたS3と互換性のあるオブジェクトストレージで、MinIOがサポートしているAPIであれば直接MinIOへリクエストを投稿してテストする方法も取れる。 この辺はよしなに使い分けたいところ。

MinIOについては過去にブログで取り上げたので興味がある方はどうぞ。

MinIOを触ってみた - rsym’s diary