ツール | |

MochaとChaiでなんでもテスト ~WebAPI編~

MochaとChaiでなんでもテスト 3本目です。

今回は、WebAPIのレスポンスのテストをMochaで自動化してみます。

[ Windows 10 / Node.js 10.15.3 / mocha@6.1.4 / chai@4.2.0 / request@2.88.0 ]

  1. npmでrequestをインストール
  2. requestでWebAPIを叩く
  3. レスポンスをアサーション
  4. もっと細かくアサーション
  5. 同期的にテストする方法
  6. おまけ:requestでJSONをPOST
  7. 以上

npmでrequestをインストール

Node.jsでリクエストを送るにはnpmパッケージのrequestが便利なので、ここではそれを使ってAPIを叩きます。(標準ライブラリでやる方法もあるらしいのですが、筆者はやったことありません。すみません。)

テストプログラム用のプロジェクト内で以下コマンドを実行すると、プロジェクト内にrequestの最新バージョンがインストールされます。

npm install request --save-dev

この記事を書いた時点ではバージョン2.88.0でした。

requestでWebAPIを叩く

試しに叩けるAPIは無いかと探したところ、ぐるなびAPIというのがありました。このページの『レストラン検索API』を使わせてもらいます。GET で叩けて、JSON形式で返ってくるAPIみたいですね。

WebAPIにはアクセスキーとかトークンがあるのが常ですが、ぐるなびAPIでは keyid というのを使うようです。上記ページでは、日ごとに切り替わる仮の keyid が表示されてます。

このレストラン検索APIは、keyid を指定せずにアクセスすると怒られます。当然です。この時、ステータスコードは 401 が返され、レスポンスボディは以下の通りとなります。

怒られレスポンス
{
    "@attributes": {
        "api_version": "v3"
    },
    "error": [
        {
            "code": 401,
            "message": "keyidを指定してください"
        }
    ]
}

今回はこのAPIを例に、レスポンスを検証するテストプログラムを書いてみます。

※公開APIなので、レスポンス内容が変わったり、API自体が無くなったりする可能性があります。もしぐるなびAPIが無くなることがあったら、上記のレスポンス内容を念頭に、想像で読んでください。

レスポンスをアサーション

とりあえず、request を使って簡単に作ってみたものが以下のサンプルとなります。

tests/api-test.js
'use strict';

const request = require('request');
const expect = require('chai').expect;

describe('レストラン検索API', function() {
	this.timeout(10000); // デフォルトのタイムアウトが 2000ms なので 10000ms に延ばしておく
	
	it('keyid未指定', function(done) {
		request({
			url: 'https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=',
			method: 'GET',
			headers: {
				'Content-Type': 'application/json',
			}
		}, function(err, res) {
			// ステータスコード
			expect(res.statusCode).to.equal(401, 'ステータスコード');
			
			// レスポンスヘッダ (一応)
			expect(res.headers['content-type']).to.equal('application/json; charset=utf-8');
			
			// レスポンスボディ
			let body;
			try {
				body = JSON.parse(res.body);
			} catch (e) {
				expect.fail('レスポンスがJSON形式じゃない');
			}
			expect(body).to.deep.equal({
				'@attributes': {
					api_version: 'v3'
				},
				error: [
					{
						code: 401,
						message: 'keyidを指定してください'
					}
				]
			}, 'レスポンスボディ');
			
			// 終了
			done();
		});
	});
});

ポイントは以下2点です。

  • this.timeout() でタイムアウトを10,000ミリ秒に延ばす。mocha はデフォルトでは it() ごとに2,000ミリ秒でタイムアウトするので、遅くなりそうなテストでは延ばしておく。
  • done() で終了する。request のような非同期処理をテスト内で使う場合、ケース完了時に it() の第一引数から受け取ったコールバック関数 (慣例として done とする) を実行することで終了できる。

JSONパース失敗時に fail() でエラー終了させるのは、筆者はよくやりますが、もっと適切な方法があるかもしれません。it() 内でエラーが出そうな処理を行う時は、これをすると何行目でエラーが出たのか分かりやすくなります。

これを mocha で実行すると、エラー無く終了するはずです。

もっと細かくアサーション

上記の例は、レスポンスボディのアサーションがすごく大雑把です。expect().deep.equal() で一発でやってしまってます。

今回のように小さなレスポンスならこれでも大丈夫ですが、もっと大きくなると、どこでエラーが出たか分かりにくくなりそうです。なので、もう少し細かくアサーションしてみます。

レスポンスボディのアサーション部分を、以下の通りに変更します。

tests/api-test.js
// レスポンスボディ
let body;
try {
	body = JSON.parse(res.body);
} catch (e) {
	expect.fail('レスポンスがJSON形式じゃない');
}

// キーは '@attributes' 'error' だけを持つ
expect(body).to.have.all.keys('@attributes', 'error');

// @attributes はキー 'api_version' だけを持つ
expect(body['@attributes']).to.have.all.keys('api_version');

// @attributes の api_version の値は 'v3'
expect(body['@attributes'].api_version).to.equal('v3');

// error は長さ1の配列
expect(body.error).to.be.an('array').that.have.lengthOf(1);

// error の0番目はキー 'code', 'message' だけを持つ
expect(body.error[0]).to.have.all.keys('code', 'message');

// error の0番目の code の値は 401
expect(body.error[0].code).to.equal(401);

// error の0番目の message の値は 'keyidを指定してください'
expect(body.error[0].message).to.equal('keyidを指定してください');

これも同様にエラー無く終了するはずです。

同期的にテストする方法

ちなみに上記のテストプログラムですが、非同期にせずに同期的に書く方法もあります。

request をPromise化して await で実行します。分かりやすいので、個人的にはこちらの方をよく使います。

同期版
'use strict';

const request = require('request');
const expect = require('chai').expect;

/**
 * Promise化したrequest
 * リクエスト成功さえすれば、ステータスコードに関わらずresolveにレスポンスを返す
 * @param  {Object} options - requestに渡すオプション
 * @return {Promise}
 */
const pRequest = (options) => {
	return new Promise(function(resolve, reject) {
		request(options, function(error, res, body) {
			if (error) {
				reject(error);
			} else {
				resolve(res);
			}
		});
	});
};

// テスト本体
describe('レストラン検索API', function() {
	this.timeout(10000);  // タイムアウト10秒に設定
	
	it('keyid未指定', async function() {
		const res = await pRequest({
			url: 'https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=',
			method: 'GET',
			headers: {
				'Content-Type': 'application/json',
			}
		});
		
		// ステータスコード
		expect(res.statusCode).to.equal(401, 'ステータスコード');
		
		// レスポンスヘッダ
		expect(res.headers['content-type']).to.equal('application/json; charset=utf-8');
		
		// レスポンスボディ
		let body;
		try {
			body = JSON.parse(res.body);
		} catch (e) {
			expect.fail('レスポンスがJSON形式じゃない');
		}
		expect(body).to.deep.equal({
			'@attributes': {
				api_version: 'v3'
			},
			error: [
				{
					code: 401,
					message: 'keyidを指定してください'
				}
			]
		}, 'レスポンスボディ');
	});
});

おまけ:requestでJSONをPOST

こういうAPIが多いと思うので書いておきます。テスト関係ありません。

request({
	url: 'https://example.com/api/hoge',
	method: 'POST',
	headers: {
		'Content-Type': 'application/json',
	},
	body: JSON.stringify({
		id: 99,
		name: 'あああ'
	})
}, function(err, res) {
	// なんやかんや
});

以上

以上でWebAPI編は終わりです。ぐるなびさんごめんなさい。次はAWS SQS編です。