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

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

LambdaやDynamoDB、APIGatewayなどの構成をコマンド1つですぐにデプロイしてくれる便利ツールの記事の第4弾です。
保守メンテが楽になりつつも、実戦で速攻で構築ができます!!
MVC編 -> テストコード編 に変更になりました。

目次

framework_repo.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の記事とても楽しかったです。
  • また便利なのがあれば紹介したいと思います。