Edited at

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の記事とても楽しかったです。

  • また便利なのがあれば紹介したいと思います。