[Ruby][aws-sdk-core][ElasticMQ]SQSへの接続をローカルでテストする

2015.01.06

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

SQSへ接続するアプリを作成する場合、特にRSpecなどでテストをする際など、ローカルにSQSのダミーを立てて接続したいことがあるかと思います。このような場合の方法として、ダミーのSQSとしてElasticMQを使い、アプリ本体のソースからはAWS上のSQSに接続・RSpecからはローカルのElasticMQに接続するというやり方があります。今回、そのサンプルを作成してみました。

※SQSへの接続に、aws-sdk-coreを使用しました。後述しますが、一部、aws-sdk-coreへのモンキーパッチを当てた部分があります。この部分に関しては、aws-sdk-coreのバージョンが異なれば必要ないかもしれません(今回のバージョンは"2.0.17")。またaws-sdk-coreはV1のAWS SDK for Rubyとは違うものであることにも注意してください。

事前準備

1.ElasticMQ

ElasticMQのサイトよりjarをダウンロードしてください。そして以下のコマンドでElasticMQを起動しておきます。

$ java -jar elasticmq-server-0.8.5.jar

プロジェクトの作成

$ bundle init 等でGemfileを用意した後、必要となるgemを記述し、$ bundle installを行います。

Gemfile
source 'https://rubygems.org'

gem 'aws-sdk-core'

group :development,:test do
  gem 'rspec'
  gem 'pry-byebug'
end

SQSに接続するための「aws-sdk-core」、テストをするための「rspec」をインストールします。(「pry-byebug」はデバッグで使いたい方のみ入れてください。)

ソースコード

AWS上のSQSに対して以下の処理を行うソースと、それをテストするRSpecを作成しました。

  • キューの作成
  • メッセージ送信
  • メッセージ受信
  • メッセージ削除
  • キューの削除

SQSへの処理

以下、上述した処理を行うソースです。

elasticmq_sample.rb
require 'aws-sdk-core'
require 'aws-sdk-core/sqs_queue_urls'

module ElasticmqSample
  class SqsSample
    def create_queue(queue_name)
      queue = client.create_queue(
        queue_name: queue_name,
      )
    end

    def send_message(queue_url, body)
      client.send_message(
        queue_url: queue_url,
        message_body: body
      )
    end

    def receive_message(queue_url)
      client.receive_message(
        queue_url: queue_url
      )
    end

    def delete_message(queue_url, receipt_handle)
      client.delete_message(
        queue_url: queue_url,
        receipt_handle: receipt_handle,
      )
    end

    def delete_queue(queue_url)
      client.delete_queue(
        queue_url: queue_url
      )
    end

    private

    def client
      @client ||= Aws::SQS::Client.new(
        region: 'us-west-2',
        profile: 'personal'
      )
    end
  end
end

上から2行で必要なモジュールを取り込んでいます。「aws-sdk-core/sqs_queue_urls」については、後述するモンキーパッチです。その下ではキュー、メッセージの操作を行うメソッドをpublicで定義しています。

一番下の「client」メソッドですが、SQSへ接続を行うためのClientオブジェクトを生成しています。実際にAWS上のSQSに対して処理を行うため、ここでは「本当」にAWSへ接続するよう定義しています。

尚、この例では~/.aws/credentialsに「personal」というプロファイル名でaccess_key_id, secret_access_keyを定義しています。

RSpec

上記の処理をテストするためのRSpecです。

elasticmq_sample_spec.rb
require 'spec_helper'
require 'aws-sdk-core'

describe ElasticmqSample do
  QUEUE_NAME = 'elasticmq_sample_queue'
  MESSAGE = 'test message.'

  let(:sqs_sample) { ElasticmqSample::SqsSample.new }
  let(:client) {
    Aws::SQS::Client.new({
      region: 'us-west-2',
      endpoint: 'http://localhost:9324',
      profile: 'personal'
    })
  }

  describe 'queue create and delete' do
    queue = nil
    receipt_handle = ''

    # beforeメソッドをコメントアウトすると、AWS上のSQSに接続する
    before(:each) do
      allow(sqs_sample).to receive(:client).and_return(client)
    end

    it 'create queue' do
      queue = sqs_sample.create_queue(QUEUE_NAME)
      expect(queue[:queue_url].include?(QUEUE_NAME)).to eq(true)
    end

    it 'send message' do
      result = sqs_sample.send_message(queue[:queue_url], MESSAGE)
      expect(result[:message_id]).not_to eq(nil?)
    end

    it 'receive message' do
      result = sqs_sample.receive_message(queue[:queue_url])
      receipt_handle = result.messages[0][:receipt_handle]
      count = result.messages.count
      expect(count).not_to eq(0)
    end

    it 'delete message' do
      result = sqs_sample.delete_message(queue[:queue_url], receipt_handle)
      expect(result.nil?).to eq(false)
    end

    it 'delete queue' do
      result = sqs_sample.delete_queue(queue[:queue_url])
      expect(result.nil?).to eq(false)
    end

  end
end

9〜15行目で、ローカルのSQSであるElasticMQに接続するClientオブジェクトを生成しています。「endpoint〜」で接続先をローカルにしているだけですね。

このClientオブジェクトを使用するよう、23行目で設定しています。「allow」を使い、テスト対象となるソース(elasticmq_sample.rb)の「client」メソッドが返却するオブジェクトとして、RSpec内で生成したClientオブジェクトを指定しています。

なのでこの23行目をコメントアウトすると、テスト対象となるソース内で生成するClientオブジェクトが使われるため、テストの実行時にもAWS上のSQSへ接続が行われます。

モンキーパッチ

最後にモンキーパッチについてです。今回使用したaws-sdk-core 2.0.17では、ローカルのElasticMQに対して「send message」を行ったときに以下のエラーが発生しました。

ArgumentError:
       invalid queue url `http://localhost:9324/queue/elasticmq_sample_queue'

原因はaws-sdk-coreの以下の部分です。

〜/ruby/2.1.0/gems/aws-sdk-core-2.0.17/lib/aws-sdk-core/plugins/sqs_queue_urls.rb

module Aws module Plugins # @api private class SQSQueueUrls < Seahorse::Client::Plugin class Handler < Seahorse::Client::Handler (中略) def update_region(context, url) if region = url.to_s.split('.')[1] context.config = context.config.dup context.config.region = region context.config.sigv4_region = region else raise ArgumentError, "invalid queue url `#{url}'" end end end (中略) end end end [/ruby]

この「update_region」メソッドですが、urlよりregion名を取得し、nilの場合はエラーを発生させています。「本物」のSQSに接続する場合のurlは「https://sqs.us-west-2.amazonaws.com/xxxx/elasticmq_sample_queue」となりregionを取得できますが(この場合では「us-west-2」)、ローカルのElasticMQの場合は「http://localhost:9324」となりregionを取得できません。このためエラーが発生していました。

このエラーを回避するため、以下のようなモンキーパッチを作成しました。

aws-sdk-core/sqs_queue_urls.rb

module Aws module Plugins # @api private class SQSQueueUrls < Seahorse::Client::Plugin class Handler < Seahorse::Client::Handler def update_region(context, url) if region = url.to_s.split('.')[1] context.config = context.config.dup context.config.region = region context.config.sigv4_region = region elsif url.include?('localhost') return else raise ArgumentError, "invalid queue url `#{url}'" end end end end end end [/ruby]

選択してある所が、今回追加したソースです。urlに「local」がある(つまりローカルのElasticMQに接続したい)場合、contextを上書きせずに処理を終了しています。

まとめ

RSpecのテストを実行する際に、AWS上のSQSに接続する代わりにローカルのElasticMQに接続するだけでしたが、結構手間取りました。「allow」によるテスト対象の置き換え、モンキーパッチによるgemの修正など、RubyやRSpecの面白い部分を使うことが出来たように思います。

キューの作成・削除、メッセージの送受信・削除と限定した操作しか行っていませんが、SQSをローカルでテストする際に少しでも役に立てれば幸いです。

備考

今回のソースコードは以下のGithubに上げてあります。必要な方は、参考にしてください。
elasticmq_sample