serverless frameworkを使って本格的なAPIサーバーを構築(テストコード編)
LambdaやDynamoDB、APIGatewayなどの構成をコマンド1つですぐにデプロイしてくれる便利ツールの記事の第4弾です。
保守メンテが楽になりつつも、実戦で速攻で構築ができます!!
MVC編 -> テストコード編 に変更になりました。
目次
- serverless frameworkを使って本格的なAPIサーバーを構築(魅力編)
- serverless frameworkを使って本格的なAPIサーバーを構築(ハンズオン編)
- serverless frameworkを使って本格的なAPIサーバーを構築(Express編)
- serverless frameworkを使って本格的なAPIサーバーを構築(テストコード編)← 今ここ
前回まで、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の記事とても楽しかったです。
- また便利なのがあれば紹介したいと思います。