AWS SDKでも単体テストを書きたい！

この記事は、[OthloTech Advent Calendar 2018](https://qiita.com/advent-calendar/2018/othlotech) 10日目の記事として書かれています。

## TL;DR
 - Pythonでboto3(AWS SDK)を使う時には[moto](https://github.com/spulec/moto)を使おう。
 - 機能が足りない時はISSUEを書いたり、余裕があればプルリクエストを送ろう。

## AWS SDKとは
AWS SDKとはAWSの機能をプログラムから制御するために利用するライブラリを指します。AWSコンソールを利用して行うことは概ね行うことができるのでアプリケーションがAWSを直接的に利用する際等(S3のファイルにアクセス、インスタンスを立てる等)に利用されます。リンク先に記述があるように多くの言語で利用することが可能です。

https://aws.amazon.com/jp/tools/

自分はLambdaでAWSリソースを制御したい時に使うことが多いです。また、[AWS-CLI](https://aws.amazon.com/jp/cli/)がPythonやSDKであるboto3に依存していて安定していそうな気がするということからPythonを使うことが多いです。したがって本記事ではPythonのAWS SDKであるboto3についてのみ言及しています。

### AWS SDKの開発をしていて辛かったこと
AWS SDKではリソースを作成するようなプログラムを作成することがそれなりにありましたが、課金対象になってしまうのであまりテストプログラムを書こうとは思えませんでした。また、VMを立てるようなプログラムの場合仮にテストを行ったとしてもかなり時間がかかってしまいます。CIで実行するならまだましですが、手元で実行することもあるので待ち時間があるのは厳しいです。

さらに言えば個人的には開発手法としてのテスト駆動開発のような細かい目標地点がわかりやすいテストが好きなこと、CIツールにアラートを出されるとなんとなく守られてる気がするのが嬉しいというのもありテストは書きたかったです。

## Motoとは
OSSでboto3向けに開発されているモックライブラリです。所定の宣言を行うことでboto3を実行した時にそれっぽい値を返却するようになります。また、その時AWSにはアクセスしません。リポジトリは下記です。

https://github.com/spulec/moto

なお、他の言語でもある程度モックプログラムが開発されているらしいので興味がある人は探してみてくださいね。

## 使い方
### 前準備

パッケージのインストールをしましょう。環境によって動かし方は異なると思います。基本Python3の方が良いと思います。

```bash:prerequirement
pip3 install moto
```

### テスト対象

例えば下記の2つの関数のテストがしたいとします。

```python:boto3_example.py
import boto3

def get_s3_file_body():
    bucket_name = 'hoge'
    file_name = 'fuga.txt'
    s3 = boto3.resource('s3')
    response = s3.Object(bucket_name, file_name).get()
    return response['Body'].read().decode('utf-8')

def stop_instance(instance_ids):
    ec2_client = boto3.client('ec2')
    ec2_client.stop_instances(InstanceIds=instance_ids)

```

`get_s3_file_body`がs3の`hoge`というバケットの`fuga.txt`の中身を取得するプログラムです。`stop_instance`が引数で与えられたInstanceIdのインスタンスを停止させるプログラムです。まあ、なんとなくイメージしやすいプログラムなんじゃないでしょうか。

### テストプログラム 
サクッとunittestを使って作ることにします。Pipenvが出て、テスト専用のライブラリを導入しやすくなりましたが、標準で含まれているunittestは使う分には楽でいいですね。

```python:test_boto3_example.py
import os
import unittest
import boto3
import moto
from moto.ec2.models import AMIS
from boto3_example import get_s3_file_body, stop_instance


os.environ['AWS_DEFAULT_REGION'] = 'ap-northeast-1'

class TestBoto3Example(unittest.TestCase):
    @moto.mock_s3
    def test_get_s3_file_body(self):
        bucket_name = 'hoge'
        file_name = 'fuga.txt'
        expected = 'foobar'
        s3 = boto3.resource('s3')
        s3.create_bucket(Bucket=bucket_name)
        s3.Object(bucket_name, file_name).put(Body=expected)
        self.assertEqual(expected, get_s3_file_body())
    
    @moto.mock_ec2
    def test_stop_instance(self):
        # make instance
        ec2_client = boto3.client('ec2')
        ec2_client.run_instances(ImageId=AMIS[0]['ami_id'], MinCount=2, MaxCount=2)
        # get ids
        full_info = ec2_client.describe_instances()
        instance_ids = []
        for r in full_info['Reservations']:
            for i in r['Instances']:
                # check running
                self.assertEqual('running', i['State']['Name'])
                instance_ids.append(i['InstanceId'])
        
        # stop instances
        stop_instance(instance_ids)
        # check stopping
        full_info = ec2_client.describe_instances()
        for r in full_info['Reservations']:
            for i in r['Instances']:
                # check running
                self.assertEqual('stopped', i['State']['Name'])
                self.assertTrue(i['InstanceId'] in instance_ids)


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

```

さて、リージョンについてですが、ec2ではデフォルトのリージョンが設定されていない場合にはエラーが発生するため普段環境変数に持たせていたり、Lambdaでの運用が前提の場合にはテストプログラムで環境変数を使うといいと思います。

利用する際には関数の前に利用する機能デコレータを設定することでboto3はAWSにアクセスするのではなく、motoを見に行くようになります。テストしたい関数に合わせて設定しましょう。特別なコードとしてはこれだけで、あとは普通にテストプログラムを各要領でプログラムを書けばOKです。

`test_get_s3_file_body`ではS3にアップロードして、それを`get_s3_file_body`で読み込み、書き込んだ内容と比較しています。`test_stop_instance`ではインスタンスを作成して、そのIDを確認するとともに動作していることを確認しています。次に`stop_instance`を動作させます。最後に状態として、停止状態にあることとInstanceIdに変更がないことを確認しています。

### 実行してみる
普通にunittestなのでそのまま実行します。

```bash:result
➜  python -m unittest discover
..
----------------------------------------------------------------------
Ran 2 tests in 0.511s

OK
```

正常に動作することが確認できました！

### 注意事項
CloudWatchのメトリクス取得部分などまだまだ実装されていない機能も多く存在します。気になるかたISSUEで連絡したり、OSSということでご自身で実装するのも面白いかと思います。

## まとめ
motoを使うとboto3のテストが大きな手間なく行えます！
motoを使ってboto3を使ってプログラムでもテストプログラムを書こう！