serverless frameworkを使って本格的なAPIサーバーを構築（テストコード編）


# serverless frameworkを使って本格的なAPIサーバーを構築（テストコード編）
LambdaやDynamoDB、APIGatewayなどの構成をコマンド１つですぐにデプロイしてくれる便利ツールの記事の第４弾です。
保守メンテが楽になりつつも、実戦で速攻で構築ができます！！
~~MVC編~~ -> テストコード編　に変更になりました。

## 目次
- [serverless frameworkを使って本格的なAPIサーバーを構築（魅力編）](https://qiita.com/okamu_/items/e246887cb6f2621ad40a)
- [serverless frameworkを使って本格的なAPIサーバーを構築（ハンズオン編）](https://qiita.com/okamu_/items/0f1055e5c91ed99ebec6)
- [serverless frameworkを使って本格的なAPIサーバーを構築（Express編）](https://qiita.com/okamu_/items/c19e2a682afe41a47eb1)
- **[serverless frameworkを使って本格的なAPIサーバーを構築（テストコード編）](https://qiita.com/okamu_/items/2967726eef27420e6c63)←　今ここ**


![framework_repo.png](https://qiita-image-store.s3.amazonaws.com/0/62822/e9ffa044-cdaf-c024-2dba-b670407ab1dd.png)


前回まで、serverless frameworkを使い、AWS Lambda上でExpress動かす記事まで書きました。
今回は、serverless frameworkで 「 **lambda + APIGateway + DynamoDB** 」　の構成で、**Express**を動かしテストできる構成にします。

## この記事でできるようになること
- どこでも実行できる**テストを書く**
    - **DynamoDBのテストをStabを使って実現する**

## DynamoDB stabを使う
- ローカルでテストを行う場合に毎回AWSリソースにアクセスさせる必要はないため、スタブ化してダミーの値を返しつつ、どういう値で呼ばれたか、テスト内で監視を行うことができる。
    - ローカルやテストコードの実行、外部テスト（CircleCIなどのCIツール）で便利です。

## 前準備
- テスト用のnpm moduleをinstallしておきます。

```
chai lambda-tester mocha sinon aws-sdk-mock supertest
```

## 構築する
- ディレクトリ構成はこんな感じになりました。

```
$ tree
├── functions
│   ├── api
│   │   ├── api.js
│   └── routes
│       ├── hoge.js
│       └── fuga.js
├── models
│   ├── hoge-users.js
│   └── huga-users.js
├── serverless.yml
├── tests
│   ├── functions
│   │   ├── hoge-test.js
│   │   └── huga-test.js
│   ├── lib
│   │   └── dynamo-stub.js
│   └── models
│       ├── hoge-users-test.js
│       └── fuga-users-test.js

```

### stub化する
- 以下のようにすることで、dynamodbのgetやputなどはstub化されます。

```tests/lib/dynamo-stub.js
'use strict'

const aws = require('aws-sdk-mock');
const path = require('path');
const chai = require('chai');
const should = chai.should();
aws.setSDK(path.resolve('node_modules/aws-sdk'));
process.on('unhandledRejection', console.dir);

function create() {
  aws.mock('DynamoDB.DocumentClient', 'put', (params, callback) => {
    callback(null, 'successfully put item in database');
  });

  aws.mock('DynamoDB.DocumentClient', 'get', (params, callback) => {
    callback(null, { Item: { request_token: 'UTPZgZTuL4cqYIjxvQFHFTdfuaIONLrp' , request_secret: '5RGcFAAAAAAA1x1BAAABXd68Yr0'} });
  });

  aws.mock('DynamoDB', 'describeTable', (params, callback) => {
    const desc = {
      Table: {
        AttributeDefinitions: [ [Object] ],
        TableName: params.TableName,
        ProvisionedThroughput: {
          NumberOfDecreasesToday: 0,
          ReadCapacityUnits: 1,
          WriteCapacityUnits: 1
        },
        TableSizeBytes: 0,
        ItemCount: 0,
        TableArn: 'arn:aws:dynamodb:ap-northeast-1:12345678:table/aaaa'
      }
    };
    callback(null, desc);
  });
}

module.exports = {
  create
};
```


### api.js
- api.jsはいつもと変わりなくroutesにあるfunctionを、APIのPathに合わせて読んであげます。
- この場合は、`api/v1/`に対して2つのfunctionが呼ばれるようになっています。

```functions/api/api.js
'use strict';

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const hoge = require('../routes/hoge');
const fuga = require('../routes/fuga');

app.use(bodyParser.urlencoded({
  extended: true
}));
app.use(bodyParser.json());

app.use('/api/v1', hoge);
app.use('/api/v1', fuga);

exports.handler = require('express-on-serverless')(app);

if (process.env.NODE_ENV === 'test') {
  module.exports = app;
}
```

### function
- コントローラーに当たるfunctionは任意のPathがきたら実行されるようにしておきます
- この場合は、`api/v1/hoge`に該当するAPIがきた場合に実行されます。
- 戻り値はjsonなので、特にViewは作っていませんので、ここで返します。

```functions/routes/hoge.js
const express = require('express');
const HogeUsers = require('../../models/hoge-users');
const router = express.Router();

router.post('/hoge', (req, res) => {
  let user = new HogeUsers();
  user.create(req)
    .then((hash) => {
      if (hash.error) {
        res.status(hash.code).json({
          message: hash.message
        });
      }
      else {
        res.json({
          message: hash.message
        });
      }
    })
    .catch((err) => {
      console.error('Internal Server Error');
      console.error(err);
      res.status(500).json({
        message: 'Internal Server Error'
      });
    });
});

module.exports = router;
```

### モデル
- Modelでは、DynamoDBをconstructorで自分で持っておき、データに対する操作を一任します。
- またバリデーションなどのチェックも行います。

```models/hoge-users.js
'use strict';

const aws = require('aws-sdk');

class HogeUsers {
  constructor() {
    this.tableName = `hoge-users-${process.env.STAGE}`;
    this.dynamo = new aws.DynamoDB.DocumentClient();
  }

  create(userParams) {
    return new Promise((resolve) => {
     
      let permittedParams = /* いろいろチェックする(省略) */
     
      const params = {
        TableName: this.tableName,
        Item: permittedParams,
        ConditionExpression: 'attribute_not_exists(xxxxxx)'
      };

      this.dynamo.put(params, (err) => {
        if (err) {
          console.error('dynamodb put error');
          console.error(err.message);
          resolve(this._throw(err));
        } else {
          resolve({
            error: null,
            message: null,
            code: 200
          });
        }
      });
    });
  }
}

module.exports = HogeUsers;
```

### functionのテスト
- ここでは、/api/v1/hogeに対してのテストを書きます。
- この場合は、400が帰ってくることをテストします。

```tests/functions/hoge-test.js
'use strict';

const LambdaTester = require('lambda-tester');
const api = require('../../functions/api/api');
const request = require('supertest');
const dynamoStub = require('../lib/dynamo-stub');

describe('functions hoge Tests', function () {
  this.timeout(0);
  beforeEach(function () {
    dynamoStub.create();
  });

  // /api/v1/hoge
  it('hoge fail post test', (done) => {
    request(api)
      .post('/api/v1/hoge')
      .send({deviceType: 'ios'})
      .expect(400, done);
  });
});

```

## モデルのテスト
- モデルのテストでは、モデルクラスのテストを書きます。
- この場合は、バリデーションがうまくいっているか検証をしています（省略）

```tests/models/hoge-users-test.js
'use strict';

const enc = require('../../lib/encrypt');
const stub = require('../lib/dynamo-stub');
let hogeUsers;
const HogeUsers = require('../../models/hoge-users');

describe('hoge users Tests', function () {
  this.timeout(0);
  beforeEach(function () {
    stub.create();
    hogeUsers = new HogeUsers();
  });


  it('hoge users validate HOGE', (done) => {
    /* いろいろチェックする(省略) <- のテストとか */
    done();
  });


```

## テストを実行する
- 最後に、package.jsonにテストを走らせるスクリプトを記述します。

```package.json
"scripts": {
    "test": "export NODE_PATH=`npm root -g` && NODE_ENV=test mocha -t 100000 tests/**/*-test.js"
  }
```
- 実行してみましょう


```
$ yarn test
```
- テストが成功するとこんな感じになるはずです。

```

  hoge users Tests
    ✓ hoge name
    ✓ hoge users describeTable
    ✓ hoge users xxxxx HOGE
    ✓ hoge users xxxxx HOGE
    ✓ hoge users XXXXX check yyyy
    ✓ hoge users validate HOGE
    ✓ hoge users validate check yyyy

  fuga users Tests
    ✓ fuga name
    ✓ fuga users XXXXXX
    ✓ fuga users yyyy
    ✓ fuga users validate


  27 passing (1s)

✨  Done in 4.19s.
```

## まとめ
- aws-mockを使い、DynamoDBのstub化ができました。
- modelクラスにDynamoDBを持つことで、functionのコード量が減りました。

## さいごに
- serverless frameworkの記事とても楽しかったです。
- また便利なのがあれば紹介したいと思います。