Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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の記事とても楽しかったです。
  • また便利なのがあれば紹介したいと思います。
okamu_
no plan inc. CEO 元フリーランスエンジニア/ iOS / サーバーサイド / 共同創業 / 福岡出身 https://qiita.com/organizations/noplan-inc
https://twitter.com/okamu_ro
admin-guild
「Webサービスの運営に必要なあらゆる知見」を共有できる場として作られた、運営者のためのコミュニティです。
https://admin-guild.slack.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした