AWS SDK for Rubyのクライアントをスタブ化する

AWS SDKを使用する機能をテストする際、そのままテストすると「都度AWSへのアクセスが発生してしまう」ということを熟慮しなければなりません。テストの内容・頻度によっては思わぬ額の料金請求が発生したり、AWS側からペナルティを受けたりする懸念があります。

本記事では、上記問題の回避策としてAWS SDKを使用する機能をテストする際にクライアントをスタブ化する方法をまとめます。

# 例題
以下のように、AWSへアクセスする関数`start_instances`をテストするケースを想定します。

```ruby:aws_sample.rb
require 'aws-sdk'
require 'minitest/autorun'

# テスト対象の関数
def start_instances
  # EC2のクライアントを生成する
  credential = Aws::AssumeRoleCredentials.new(region: REGION, client: Aws::STS::Client.new,
                                              role_arn: ROLE_ARN, role_session_name: 'sample_client')
  client = Aws::EC2::Client.new({credentials: credential})

  # EC2のインスタンスを起動する
  resp = client.start_instances(instance_ids: [EC2_INSTANCE_ID])
end

# テストコード
class Ec2StartTest < Minitest::Test
  def test_ec2_start
    resp = start_instances
    assert resp != nil
  end
end
```

このまま実行してもテストはPASSします。
ただし、テストを実行するごとに`client.start_instances`によるAWSとの通信が発生します。
AWSとの通信を発生させずに、テストする方法を考えてみましょう。

# 解決方法
## 1. Aws.configを使用する
事前に`Aws.config[:stub_responses] = true`を実行することで、 `Aws::EC2::Client.new`で生成されるクライアントをスタブ(AWSとは通信をせずに結果を応答するオブジェクト)とすることができます。

「コードを1行追加するだけ」とお手軽な方法である反面、AWS SDKによって生成される全クライアントがスタブとなるため、**特定のクライアントのみをスタブ化することはできない点に注意が必要**です。

```ruby:aws_sample.rb
require 'aws-sdk'
require 'minitest/autorun'

if Rails.env.test?
  Aws.config[:stub_responses] = true # !!!追加
end

# テスト対象の関数
def start_instances
  # EC2のクライアントを生成する
  credential = Aws::AssumeRoleCredentials.new(region: REGION, client: Aws::STS::Client.new,
                                              role_arn: ROLE_ARN, role_session_name: 'sample_client')
  client = Aws::EC2::Client.new({credentials: credential})

  # EC2のインスタンスを起動する
  resp = client.start_instances(instance_ids: [EC2_INSTANCE_ID])
end

# テストコード
class Ec2StartTest < Minitest::Test
  def test_ec2_start
    resp = start_instances
    assert resp != nil
  end
end
```

## 2. Client生成時のオプションを変更する
クライアント生成時の引数を変更することで、生成されるクライアントをスタブ化することができます。

```ruby:aws_sample.rb
require 'aws-sdk'
require 'minitest/autorun'

# テスト対象の関数
def start_instances
  # EC2のクライアントを生成する
  credential = Aws::AssumeRoleCredentials.new(region: REGION, client: Aws::STS::Client.new,
                                              role_arn: ROLE_ARN, role_session_name: 'sample_client')

  if Rails.env.test?
    client = Aws::EC2::Client.new(stub_responses: true) # !!!追加
  else
    client = Aws::EC2::Client.new({credentials: credential})
  end

  # EC2のインスタンスを起動する
  resp = client.start_instances(instance_ids: [EC2_INSTANCE_ID])
end

# テストコード
class Ec2StartTest < Minitest::Test
  def test_ec2_start
    resp = start_instances
    assert resp != nil
  end
end
```

# スタブの戻り値を設定する
上記方法で作成したスタブは、メソッド呼び出し時に何かしらの戻り値を返すものの、その内容はいわゆる**空の応答**です。このため、以下のように応答の内容を参照する場合において意図しない動作となります。

```ruby:aws_sample.rb
require 'aws-sdk'
require 'minitest/autorun'

if Rails.env.test?
  Aws.config[:stub_responses] = true
end

# テスト対象の関数
def start_instances
  # EC2のクライアントを生成する
  credential = Aws::AssumeRoleCredentials.new(region: REGION,
                                              client: Aws::STS::Client.new,
                                              role_arn: ROLE_ARN,
                                              role_session_name: 'sample_client')
  
  client = Aws::EC2::Client.new({credentials: credential})

  # EC2のインスタンスを起動する
  resp = client.start_instances(instance_ids: [EC2_INSTANCE_ID])
end

# テストコード
class Ec2StartTest < Minitest::Test
  def test_ec2_start
    resp = start_instances
    assert resp != nil
    assert_equal 'i-xxxx', resp.starting_instances[0].instance_id # !!!応答を参照する処理
  end
end
```

有効な応答を返してもらう時は`client.stub_responses`を事前に呼び出し、第1引数に呼び出すメソッドのシンボルを、第2引数に返して欲しい応答の内容を設定します。

```ruby:aws_sample.rb
require 'aws-sdk'
require 'minitest/autorun'

if Rails.env.test?
  Aws.config[:stub_responses] = true
end

# テスト対象の関数
def start_instances
  # EC2のクライアントを生成する
  credential = Aws::AssumeRoleCredentials.new(region: REGION,
                                              client: Aws::STS::Client.new,
                                              role_arn: ROLE_ARN,
                                              role_session_name: 'sample_client')
  
  client = Aws::EC2::Client.new({credentials: credential})

  if Rails.env.test?
    # スタブに応答を設定する
    client.stub_responses(:start_instances, { starting_instances: [ { instance_id: 'i-xxxx' } ] }) # !!!追加
  end

  # EC2のインスタンスを起動する
  resp = client.start_instances(instance_ids: [EC2_INSTANCE_ID])
end

# テストコード
class Ec2StartTest < Minitest::Test
  def test_ec2_start
    resp = start_instances
    assert resp != nil
    assert_equal 'i-xxxx', resp.starting_instances[0].instance_id # !!!応答を参照する処理
  end
end
```

# 注意
上記方法はクライアントを**スタブ**化する方法です。
**モック**ではない(≒クライアントに対するメソッド呼び出し回数や引数の検証は実施しない)点に注意しましょう。

# まとめ
AWS SDK for Rubyのクライアントは上記の通り容易にスタブ化することができます。
スタブを上手く活用して、テスト時間短縮、不要なアクセス回避を心がけるようにしましょう。