Edited at

AWS LambdaをTest Runnerとして使ってみる


はじめに

Serverless Advent Calendar 2017 の6日目です。

サーバーレスの技術を使うとWeb APIの開発が非常にお手軽になりました。

ところで質問。

ちゃんとテスト書いてますか??

この記事では、サーバーレスで作ったWeb API (別にサーバーレスで作ってなくてもいいけど) のAPIテストをサーバーレスでやっちゃおう、という小ネタです。 実運用ができるかどうかはやってみないとわかりません。


Web APIのテストって?

デプロイ後のWeb API、皆さんはどのようにテストされていますか?


  • curlなどを使ってShellScrpitでテスト組む?

  • いわゆるxUnit系のテストフレームワークを使ってテストする?

  • 何かしらのSaaSを使ってテスト?

  • Rest Client (Postmanなど) を使って手動でテスト^^;

などでしょうか?

ここで提案です。

サーバーレスでやればいいんじゃね??


AWS Lambdaをテストランナーとして使う


概要

Nodeでやります。

JavaScriptのテストフレームワークは、MochaやJasmine、Jestなどが有名です。最近だとAVAが流行っていますかね。 これらは基本的にはLocal環境でテストしたり、CI/CD環境でテストをまわすのに利用されます。

が、今回は上記のようなテストフレームワークは一切使わず、Lambdaでテストを書いてみます。


テスト対象のAPI

すごーくシンプルな、下記のようなCRUDなAPIを想定します。

API
description

POST /users
ユーザリソース作成

GET /users/{userId}
ユーザリソース取得

PUT /users/{userId}
ユーザリソース更新

DELETE /users/{userId}
ユーザリソース削除


テストを書く!

テストランナーはLambdaにするとしても、Web APIをたたいたり、Assertionなどは外部パッケージに頼ります。

とりあえず定番の supertestshould を使います。

コードの内容としては以下のようなものになっています。


  1. POST /users で34歳のTaroというユーザを作成し、

  2. GET /users/{userId} でユーザ情報を参照し、

  3. PUT /usert/{userId} でTaro -> Hanakoに改名し、

  4. DELETE /users/{userId} でユーザを削除する

const Supertest = require('supertest');

const should = require('should');

const agent = Supertest.agent('https://your-api-host/your-api-resource-path');

const testShouldPostUserSucceeds = () => {
const user = {
name: 'Taro',
age: 34
};

return new Promise((resolve) => {
agent.post('/users')
.set('Accept', 'application/json')
.send(user)
.expect((res) => {
res.status.should.equal(200);
res.body.should.hasOwnProperty('userId');
res.body.name.should.equal('Taro');
res.body.age.should.equal(34);
}).end((err, res) => {
if (err) {
console.log('[Failed] testShouldPostUserSucceeds');
throw err;
}
console.log('[Passed] testShouldPostUserSucceeds');
resolve(res.body.userId);
});
});
}

const testShouldGetUserSucceeds = (userId) => {
return new Promise((resolve) => {
agent.get(`/users/${userId}`)
.set('Accept', 'application/json')
.expect((res) => {
res.status.should.equal(200);
res.body.userId.should.equal(userId);
res.body.name.should.equal('Taro');
res.body.age.should.equal(34);
}).end((err, res) => {
if (err) {
console.log('[Failed] testShouldGetUserSucceeds');
throw err;
}
console.log('[Passed] testShouldGetUserSucceeds');
resolve(res.body.userId);
});
});
}

const testShouldPutUserSucceeds = (userId) => {
const user = {
name: 'Hanako',
age: 34
}

return new Promise((resolve) => {
agent.put(`/users/${userId}`)
.set('Accept', 'application/json')
.send(user)
.expect((res) => {
res.status.should.equal(200);
res.body.userId.should.equal(userId);
res.body.name.should.equal('Hanako');
res.body.age.should.equal(34);
})
.end((err, res) => {
if (err) {
console.log('[Failed] testShouldPutUserSucceeds');
throw err;
}
console.log('[Passed] testShouldPutUserSucceeds');
resolve(res.body.userId);
});
});
}

const testShouldDeleteUserSucceeds = (userId) => {
return new Promise((resolve) => {
agent.delete(`/users/${userId}`)
.set('Accept', 'application/json')
.expect((res) => {
res.status.should.equal(200);
res.body.userId.should.equal(userId);
}).end((err, res) => {
if (err) {
console.log('[Failed] testShouldDeleteUserSucceeds');
throw err;
}
console.log('[Passed] testShouldDeleteUserSucceeds');
resolve();
});
});
}

exports.handler = (event, context) => testShouldPostUserSucceeds()
.then(userId => testShouldGetUserSucceeds(userId))
.then(userId => testShouldPutUserSucceeds(userId))
.then(userId => testShouldDeleteUserSucceeds(userId))
.then(() => {
context.succeed('Test Passed');
}).catch(() => {
context.fail('Test Failed');
});


  • Node 6.x でTranspile無しで動くように書いています。

  • ESLintにはめちゃくちゃ怒られる書き方になっているのでご注意を。

  • Supertestなどの説明は省いているので他の記事をご参照ください。


これをLambdaで実行するとどうなるか?


テスト成功時

上記のコードをLambdaで実行し、テストに全部成功すると、例えばAWSのConsoleで確認すると以下の図のようになります。


テスト失敗時

一方で、 testShouldPutUserSucceeds をちょっといじって、Taro -> Hanakoの改名に失敗するような、Failするテストに変更してみると、

  res.status.should.equal(200);

res.body.userId.should.equal(userId);
// res.body.name.should.equal('Hanako');
res.body.name.should.equal('Taro');
res.body.age.should.equal(34);

下記の図のように、失敗したテストがLambdaの実行ログでわかるのです!!


所感

これはいったい誰トクなのか?

たぶん下記のようなメリットがあるはず。


  • CloudWatch Eventsなどから定期実行できる

  • 定期的に動かせば、APIの死活監視にも使える

  • Lambdaの実行結果をとりあえずSNSにでも飛ばしておけば、メールだったりWebhookでどっかにポストしたり、通知の柔軟性が高い

ただ、冒頭に書いた通り、本当にこんなものが必要なのかはよくわからない。

もし気が向いたら (需要がありそうなら) フレームワーク化してOSS化を検討したいところ。