Serverless Frameworkで行う継続的インテグレーションのススメ その2 ~DynamoDBに接続しての自動テスト~

## 概要
[Serverless Frameworkで行う継続的インテグレーションのススメ](http://qiita.com/horike37/items/15bf675f973d30bb1f5c)にてServerlessで構築したプロジェクトに対してどのようにCIを回していくのかを紹介しました。

しかし、上記で紹介したのはLambdaの実行によるテストしか考慮がされていません。実際にはDynamoDBやS3といったデータストアが存在することがほとんどだと思います。

今回はもう少し踏み込んで、実際のAWS環境上に用意したDynamoDBに対してやり取りを行うLambdaファンクションをTravisで動かして自動テストを実施する方法を紹介します。

## Travis CI専用のAWS環境の構築
Serverlessにはstageという概念が存在しており、これをステージングや本番と見立てることで環境をキレイに切り分けることが出来ます。今回は新たにユニットテスト用のstageを作ることにします。

```
$ serverless stage create -s travis -r us-east-1
```

以下のコマンドで作ったtravis用のstageにリソースを上げます。
```
$ serverless resources deploy -s travis -r us-east-1
```

DynamoDBを使ったリソースの定義方法は、[Serverless Frameworkを使ったAPI Gateway + Lambda + DynamoDBのセットアップ方法まとめ](http://qiita.com/horike37/items/0c18485e406063e576b7)をみてもらうと良いと思います。

今回は、usersテーブルを想定し、`${project}-${stage}-users`という命名規則でテーブルを作ってあげます。

キャプチャの通り、テーブルがプロビジョニングされました。`serverless-hkmkmh`がプロジェクト名。`travis`がstage名です。
<img width="555" alt="スクリーンショット 2016-06-17 5.05.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/65478/1c276db0-d579-f9ff-8929-4a04ba78f375.png">

## Travis CI専用のIAMユーザの追加
以下の様なポリシーを作成して、専用のIAMユーザを作ります。
DynamoDBの先ほど作ったテーブルに対して操作が可能にしてあります。

credentialは後からTravis側に設定する必要があるのでローカルにメモしておきます。



```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1466036040000",
            "Effect": "Allow",
            "Action": [
                "dynamodb:BatchGetItem",
                "dynamodb:BatchWriteItem",
                "dynamodb:DeleteItem",
                "dynamodb:DescribeTable",
                "dynamodb:GetItem",
                "dynamodb:GetRecords",
                "dynamodb:PutItem",
                "dynamodb:Query",
                "dynamodb:Scan",
                "dynamodb:UpdateItem"
            ],
            "Resource": [
                "arn:aws:dynamodb:us-east-1:<aws account id>:table/serverless-hkmkmh-travis-users"
            ]
        }
    ]
}
```

## AWS credentialのTravisへの設定
TravisからAWSリソースへアクセスするためにcredentialをTravisへ設定します。
もちろん生のcredentialを.travis.ymlへ追記することはご法度です。

Travisのコマンドにより暗号化し、credentialがTravisの環境上でしか使えないようにします。

```shell
$ travis encrypt AWS_ACCESS_KEY_ID=xxxxxxxxxxx --add
$ travis encrypt AWS_SECRET_ACCESS_KEY=xxxxxxxxxxx --add
```

このコマンドにより。`.travis.yml`に暗号化した状態でcredentialが設定されます。
これはTravis上でビルドを実行した際に環境変数として設定されます。

## テスト対象のコード
今回は、DynamoDBに対してデータをput, get, deleteするコードをテスト対象とします。
コードは以下のとおり。さらに前段にAPI　Gatewayが設置される想定で、httpリクエストを受け取る変数を判定して操作を分ける形になっています。

```js

'use strict';

var aws      = require('aws-sdk');
aws.config.update({region:process.env.SERVERLESS_REGION});
var dynamodb = new aws.DynamoDB.DocumentClient();

const stage       = process.env.SERVERLESS_STAGE;
const projectName = process.env.SERVERLESS_PROJECT;
const usersTable  = projectName + '-' + stage + '-users';

module.exports.handler = function(event, context, cb) {
  var users = new Users();

  switch (event.httpMethod) {
    case 'POST':
      users.putItem(event, context);
      break;
    case 'GET':
      users.getItem(event, context);
      break;
    case 'DELETE':
      users.deleteItem(event, context);
      break;
  }
};

class Users {

  putItem(event, context) {
    var params = {
      TableName: usersTable,
      Item: { 'email': event.email }
    };
   
    dynamodb.put(params, function (err, data) {
      if (err) {
        context.fail(data);
      } else {
        context.succeed('putItem succeed');
      }
    });
  }

  getItem(event, context) {
    var params = {
      TableName: usersTable,
      Key: { "email": event.email }
    };

    dynamodb.get(params, function (err, data) {
      if (err) {
        context.fail(err);
      } else {
        context.succeed(data);
      }
    });
  }
  
  deleteItem(event, context) {
    var params = {
      TableName: usersTable,
      Key: { "email": event.email }
    };

    dynamodb.delete(params, function (err, data) {
      if (err) {
        context.fail(err);
      } else {
        context.succeed('deleteItem succeed');
      }
    });
  }
}
```

## テストコード
テストコードは以下のとおりです。
put,get,deleteに対してそれぞれ返り値をチェックしてあげています。

```js
'use strict';
// Unit tests for Serverless
// Generated by Serverless UnitTestBoilerplate
var s
const path       = require('path'),
      chai       = require('chai'),
      should     = chai.should(),
      Serverless = require('serverless'),
      region     = 'us-east-1',
      stage      = 'travis'
      
describe('ServerlessProjectTest', function() {
  beforeEach(function(done) {
    this.timeout(0);

    s = new Serverless();

    s.init().then(function() {
      s.config.projectPath = __dirname + '/../';
      s.setProject(new s.classes.Project({
        name:'serverless-hkmkmh',
        stages: {
          travis: { regions: { 'us-east-1': {} }}
        },
        variables: {
          project: 'serverless-hkmkmh',
          stage:   'travis',
          region:  'us-east-1'
        }
      }));
      
      s.getProject().setFunction(new s.classes.Function(
        {
          name:"users",
          runtime:"nodejs4.3"
        },
        __dirname + '/../users/s-function.json'));
 
      done();
    });
    
  });
  
  describe('#funciton users()', function() {
    it('should be funciton users put item success', function() {
      return s.getProject().getFunction('users').run('travis', 'us-east-1', {'httpMethod':'POST', 'email':'horike@digitalcube.jp'})
      .then(result => {
        result.response.should.equal('putItem succeed')
      });
    });
    
    it('should be funciton users get item success', function() {
      return s.getProject().getFunction('users').run('travis', 'us-east-1', {'httpMethod':'GET', 'email':'horike@digitalcube.jp'})
      .then(result => {
        result.response.Item.email.should.equal('horike@digitalcube.jp')
      });
    });
    
    it('should be funciton users delete item success', function() {
      return s.getProject().getFunction('users').run('travis', 'us-east-1', {'httpMethod':'DELETE', 'email':'horike@digitalcube.jp'})
      .then(result => {
        result.response.should.equal('deleteItem succeed')
      });
    });
  });
});

```

## Travis上でのテストの実行
Travis上でテスト実行させると以下のとおり正しく実行できました。
ちゃんとcredentialがTravis上で展開されてAWSリソースへ接続した上での自動テストが実行されています。

https://travis-ci.org/horike37/serverless-deploy-test/builds/138190619
<img width="514" alt="スクリーンショット 2016-06-17 5.26.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/65478/285dfb5d-d446-266c-41cb-f3587a342e00.png">

今回のソース一式はGithubの[https://github.com/horike37/serverless-deploy-test](https://github.com/horike37/serverless-deploy-test)に上げています。興味があれば、見てみてください。