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

# はじめに

[Serverless Advent Calendar 2017](https://qiita.com/advent-calendar/2017/serverless) の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などは外部パッケージに頼ります。
とりあえず定番の `supertest` と `should` を使います。

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

1. POST /users で34歳のTaroというユーザを作成し、
2. GET /users/{userId} でユーザ情報を参照し、
3. PUT /usert/{userId} でTaro -> Hanakoに改名し、
4. DELETE /users/{userId} でユーザを削除する

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

<img width="900" alt="1-ok-min.png" src="https://qiita-image-store.s3.amazonaws.com/0/39743/74efb205-42bf-6cf5-0601-dfbb9ab106e2.png">

### テスト失敗時

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

```JavaScript
  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の実行ログでわかるのです！！

<img width="900" alt="2-ng-min.png" src="https://qiita-image-store.s3.amazonaws.com/0/39743/d95169b4-8e1a-c2db-4fd9-c2eedc4fc864.png">

<img width="807" alt="result.png" src="https://qiita-image-store.s3.amazonaws.com/0/39743/717142b9-abb1-ce21-e712-5f83823d172e.png">



# 所感

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

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

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


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