LocalStackを使ってテストをしてみた

2017.11.17

こんにちは。最近はAmazonEchoの招待メールをずっと待っています。城岸です。
今回は以前から少し気になっていたLocalStackを使って、pythonのテストをしてみようと思います。

本題に入る前に

LocalStackはローカルに擬似的なAWS環境を作ってくれてるツールになります。
どんな時に便利なの?という部分については@t_wadaさんがTestable Lambdaというセッションで詳しく説明されています。
とても面白いセッションなので見たことがない方は是非。
実行環境の構築方法や使い方については本ブログの以下の記事を確認してください。

LocalStackをつかってローカルでLambdaを実行してみた

環境

こんな感じの環境でテストをしています。

Mac : macOS Sierra
docker : 17.09.0-ce
docker-compose : 1.16.1
LocalStack : v0.6.0
python : 2.7.10

テスト対象のコード

今回は以下のpythonコードをテスト対象とします。
引数の値をDynamoDBのusersテーブルに書き込み、書き込み結果を戻り値とする簡単なコードです。
DynamoDBに接続するためのboto3のオブジェクトも引数として受け取ります。

put_user.py

#!/usr/bin/env python
# -*- coding: utf_8 -*-
def put_user(dynamodb, first_name, last_name):
    """
    put user in users
    """
    table = dynamodb.Table('users')
    response = table.put_item(
        Item={
            'username': (first_name+last_name).lower(),
            'first_name': first_name,
            'last_name': last_name
            }
    )
    return response

テストコード

テストにはpythonのユニットテストフレームワークであるunittestを利用します。
ポイントは3つです。

  1. エンドポイントの変更(11行目)
    エンドポイントにlocalhost(LocalStackのDynamoDBリッスンポート)を指定し、boto3のオブジェクトを作成しています。 何も指定しないとAWSのエンドポイントにリクエストが飛んでいきますが、ここに任意の値を指定することでそちらにリクエストを飛ばすことができます。
  2. テーブルの作成(15〜33行目)
    LocalStack起動時にはAWSリソースは何も存在しないため、テスト実行時の初期処理としてusersテーブルを作成しています。
  3. 期待値の確認(53〜55行目)
    ここが個人的にすごいなーと思うところ。
    LocalStackはただのmockではなく擬似的なAWS環境であるため、putリクエストに対するレスポンスやputした値などを実際に取得することができるのです。 なので、擬似的なAWSの振る舞いと期待値とを比較することでローカル環境でも結構正確なテストをすることができます。

test_put_user.py

#!/usr/bin/env python
# -*- coding: utf_8 -*-
import unittest
import boto3
from put_user import put_user

class TestPutUser(unittest.TestCase):
    """
    test class of put_user.py
    """
    dynamodb = boto3.resource('dynamodb', endpoint_url='http://localhost:4569/')

    @classmethod
    def setUpClass(cls):
        TestPutUser.dynamodb.create_table(
            TableName='users',
            KeySchema=[
                {
                    'AttributeName': 'username',
                    'KeyType': 'HASH'
                }
            ],
            AttributeDefinitions=[
                {
                    'AttributeName': 'username',
                    'AttributeType': 'S'
                }
            ],
            ProvisionedThroughput={
                'ReadCapacityUnits': 5,
                'WriteCapacityUnits': 5
            }
        )

    def test_put_user(self):
        """
        test method of put_user
        """
        first_name = 'Jane'
        last_name = 'Doe'
        username = 'janedoe'
        expected_statuscode = 200

        put_response = put_user(TestPutUser.dynamodb, first_name, last_name)

        table = TestPutUser.dynamodb.Table('users')
        get_response = table.get_item(
            Key={
                'username': username
            }
        )

        self.assertEqual(expected_statuscode, put_response['ResponseMetadata']['HTTPStatusCode'])
        self.assertEqual(first_name, get_response['Item']['first_name'])
        self.assertEqual(last_name, get_response['Item']['last_name'])

if __name__ == "__main__":
    unittest.main()

テスト実行

まずはLocalStackを起動します。環境を構築していない方はこちら

# docker-compose up
Starting localstack_localstack_1 ...
Starting localstack_localstack_1 ... done
Attaching to localstack_localstack_1
・
・
localstack_1  | Starting mock DynamoDB (http port 4569)...
・
・
localstack_1  | Ready.

テストを実行します。

$ python test_put_user.py
.
----------------------------------------------------------------------
Ran 1 test in 0.499s

OK

ローカル上の擬似的なエンドポイントを利用して、AWSのリソースに対するコードの振る舞いを確認できました!!

AWSのエンドポイントに向けて実行

テストも無事成功したので、次はAWSのエンドポイントに対してリクエストを投げて見ます。
テーブルを作成した後、以下のコードを実行してみます。
今回はboto3のオブジェクト作成時にエンドポイントは指定しません。

main.py

#!/usr/bin/env python
# -*- coding: utf_8 -*-
import boto3
from put_user import put_user

dynamodb = boto3.resource('dynamodb')
first_name = 'Jane'
last_name = 'Doe'

response = put_user(dynamodb, first_name, last_name)

print response['ResponseMetadata']['HTTPStatusCode']
$ python main.py
200

大丈夫そうです。AWSのコンソールからもデータが格納されていることが確認できました。

まとめ

あくまでも擬似的なエンドポイントであり全ての挙動がAWSと同様ということはないと思いますが、ローカル上でのテストという意味では十分利用できるような気がします。
テストコードがないプログラムは保守したくないですし、ローカル上でAWSのリソースに対するテストを実行できるのは嬉しいですね。