2020-09-14

Moto で CDK プロジェクト内の Lambda 関数をテストする #jawssonic2020

先日開催された 24時間の技術イベント JAWS SONIC 2020 のセッション内で初めて知った Moto を試してみました。

イベントページはこちら。

│JAWS SONIC 2020 & MIDNIGHT JAWS 2020

イベントは 24 時間開催ということで、 Twitter でも #jawssonic2020 のハッシュタグで大盛りあがりでした。運営のみなさま、登壇されたみなさま、本当にお疲れさまでした & ありがとうございました!

目次

JAWS-UG 長野支部のセッション

今回試す Moto に関するセッションは、 JAWS-UG 長野支部の知野さん、寺田さん、春原さんによるセッション 「AWSをMotoでmockしてユニットテスト!」 でした。登壇資料は Speakerdeck で公開されています。

Moto とは

Moto とは、 AWS の各サービスをモックできる Python のライブラリです。 Moto を使うことで AWS の各サービスを操作する Python のプロジェクトで、 AWS にアクセスする部分のテストを簡単に実装することができます。

https://github.com/spulec/moto

使ってみる

セッション内ではサーバーレス構成でのテストということで、 S3、 SQS、 SSM Parameter Store に関する例が紹介されていまいた。今回は、 EC2 に対する操作に関するテストを実装してみます。

対象のプロジェクト

今回 対象にするプロジェクトは、だいぶ前に作った下記の CDK プロジェクトです。

https://github.com/michimani/auto-start-stop-ec2

特定のタグを持つ EC2 インスタンスを自動起動・停止させる Lambda 関数と EventBridge のルールをデプロイする CDK プロジェクトです。今回はこのプロジェクト内の Lambda 関数を、 Moto を使ってテストしてみます。また、テストコードの実行には pytest を使います。

このアプリケーションの詳細については下記の記事で書いてます。

https://michimani.net/post/aws-auto-start-stop-ec2-stack-using-cdk/

テストコード

では早速 実装したテストコードを見てみます。

import boto3
import importlib
from moto import mock_ec2

MOCK_REGION = 'ap-northeast-1'
MOCK_AMI_ID = '/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-ebs'
MOCK_INSTANCE_TYPE = 't3.nano'
MOCK_INSTANCE_NAME = 'mock_instance'


@mock_ec2
class TestStartStopEc2:
    assec2 = importlib.import_module('lambda.auto-start-stop-ec2')
    ec2_client = None
    mock_instance = {}

    def setup_method(self, method):
        self.ec2_client = boto3.client('ec2', region_name=MOCK_REGION)
        res = self.ec2_client.run_instances(ImageId=MOCK_AMI_ID,
                                            InstanceType=MOCK_INSTANCE_TYPE,
                                            MinCount=1,
                                            MaxCount=1,
                                            TagSpecifications=[
                                                {
                                                    'ResourceType': 'instance',
                                                    'Tags': [
                                                        {
                                                            'Key': 'AutoStartStop',
                                                            'Value': 'TRUE'
                                                        },
                                                        {
                                                            'Key': 'Name',
                                                            'Value': MOCK_INSTANCE_NAME
                                                        }
                                                    ]}
                                            ])
        self.mock_instance = {
            'instance_id': res['Instances'][0]['InstanceId'],
            'instance_name': MOCK_INSTANCE_NAME
        }

    def teardown_method(self, method):
        self.ec2_client.terminate_instances(
            InstanceIds=[self.mock_instance['instance_id']])

    def test_stop_ec2_instance(self):
        res = self.assec2.stop_instance(self.ec2_client, self.mock_instance)
        assert res is True
        
    def test_start_ec2_instance(self):
        res = self.assec2.start_instance(self.ec2_client, self.mock_instance)
        assert res is True

Moto でモックするにはデコレータを付与するだけ

Moto では、モックしたいサービスを操作する部分 (関数、クラス) に対してデコレータをつけることで、そのサービスをモックすることができます。今回はテストコードのクラス TestStartStopEc2 に対して EC2 をモックするための @mock_ec2 デコレータをつけます。

setup/teardown でモック用インスタンスの起動と削除

クラス内の setup_method() および teardown_method() は、それぞれテストメソッドの実行前後に実行されるメソッドです。なので、 setup_method() 内でモック用の EC2 インスタンスを起動し、 teardown_method() 内でそのインスタンスを削除 (terminate) しています。

setup_method() ではインスタンスを起動したあと、起動したインスタンスの InstanceId と Instance Name (Tags の Name の値) をクラス変数に保持し、後続のテストメソッド、および teardown_method() で使用できるようにしています。

実行

テストの実行は pytest を使用します。

$ python -m pytest ./lambda/test/test_start_stop_ec2.py -v
============================================== test session starts ===============================================
platform darwin -- Python 3.8.5, pytest-6.0.2, py-1.9.0, pluggy-0.13.1 -- .venv/bin/python
cachedir: .pytest_cache
rootdir: /path/to/Projects/auto-start-stop-ec2
collected 4 items                                                                                                

lambda/test/test_start_stop_ec2.py::TestStartStopEc2::test_describe_instances PASSED                       [ 25%]
lambda/test/test_start_stop_ec2.py::TestStartStopEc2::test_get_target_ec2_instances PASSED                 [ 50%]
lambda/test/test_start_stop_ec2.py::TestStartStopEc2::test_stop_ec2_instance PASSED                        [ 75%]
lambda/test/test_start_stop_ec2.py::TestStartStopEc2::test_start_ec2_instance PASSED                       [100%]

テストの実行もできました。

まとめ

AWS の各サービスをモックできる Python のライブラリ Moto を使って CDK プロジェクト内の Lambda 関数をテストしてみた話でした。

デコレータをつけるだけで AWS の各サービスをモックできるのは、非常に簡単で便利だなと感じました。今後も Python でアプリケーションを実装する際には、 Moto の利用を考えたいと思います。

JAWS SONIC 2020 のセッション内では他にも初めて知る内容や試してみたい内容があったので、記憶が新しいうちに試していこうと思います。