5
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

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

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
5
Help us understand the problem. What are the problem?