はじめに

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/v1');

function 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);
      });
  });
}

function 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);
      });
  });
}

function 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);
      });
  });
}

function 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で確認すると以下の図のようになります。

1-ok-min.png

テスト失敗時

一方で、 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の実行ログでわかるのです!!

2-ng-min.png

result.png

所感

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

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

  • CloudWatch Eventsなどから定期実行できる
  • 定期的に動かせば、APIの死活監視にも使える
  • Lambdaの実行結果をとりあえずSNSにでも飛ばしておけば、メールだったりWebhookでどっかにポストしたり、通知の柔軟性が高い

ただ、冒頭に書いた通り、本当にこんなものが必要なのかはよくわからない。
もし気が向いたら (需要がありそうなら) フレームワーク化してOSS化を検討したいところ。