Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
6
Help us understand the problem. What is going on with this article?

More than 3 years have passed since last update.

@okamu_

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の記事とても楽しかったです。
  • また便利なのがあれば紹介したいと思います。
6
Help us understand the problem. What is going on with this article?
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
6
Help us understand the problem. What is going on with this article?