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

今回は、MochaでAWSのSQS (Simple Queue Service) にメッセージを投入するモジュールの単体テストを自動化してみます。

[ Windows 10 / Node.js 10.15.3 / mocha@6.1.4 / chai@4.2.0 / aws-sdk@2.402.0 ]

  1. npmでaws-sdkをインストール
  2. テスト対象のモジュール
  3. テストプログラム
  4. 以上

npmでaws-sdkをインストール

Node.jsでSQSキューにアクセスするには、npmパッケージのaws-sdkが必要です。

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

npm install aws-sdk --save-dev

この記事を書いた時に使ってたやつは、バージョンaws-sdk@2.402.0でした。

テスト対象のモジュール

ここではテスト対象として、SQSキューにメッセージを投入するだけの以下のようなモジュールを用意しました。

メッセージ内容は固定です。sendMyMessageToStandardQueue() でスタンダードキューに、sendMyMessageToFIFOQueue() でFIFOキューに送信します。

なお、キューはどちらも東京リージョン (ap-northeast-1) のものとします。また、FIFOキューは『コンテンツに基づく重複排除』OFF (=重複排除IDを設定して送信する必要がある) のものとします。

../mymodule.js
'use strict';

const aws = require('aws-sdk');
aws.config.update({
    region: 'ap-northeast-1',
});
const sqs = new aws.SQS();

/**
 * スタンダードキューにJSON形式のメッセージを送信
 */
const sendMyMessageToStandardQueue = async () => {
	await sqs.sendMessage({
		QueueUrl: 'https://sqs.ap-northeast-1.amazonaws.com/999999999999/my-standard-queue',
		MessageBody: JSON.stringify({
			id: 552171000001,
			name: 'kiriukun',
			age: 14,
			memo: 'standard'
		}),
	}).promise();
}

/**
 * FIFOキューにJSON形式のメッセージを送信
 * ※コンテンツに基づく重複排除=無効のキュー用。重複排除ID (MessageDeduplicationId) は適当に設定。
 */
const sendMyMessageToFIFOQueue = async () => {
	await sqs.sendMessage({
		QueueUrl: 'https://sqs.ap-northeast-1.amazonaws.com/999999999999/my-fifo-queue.fifo',
		MessageBody: JSON.stringify({
			id: 552171,
			name: 'kiriukun',
			age: 14,
			memo: 'fifo'
		}),
		MessageGroupId: 'my-group',
		MessageDeduplicationId: String((new Date()).getTime() + Math.floor(Math.random() * 100000))
	}).promise();
}

module.exports = { sendMyMessageToStandardQueue, sendMyMessageToFIFOQueue };

ところでSQSの単体テストに関して所感ですが、私は『コンテンツに基づく重複排除』ONのままテストを自動化するのはどうにもうまくできませんでした。基本的に単体テスト時のFIFOキューは、『コンテンツに基づく重複排除』OFFのほうが、同じメッセージで何度もテストができて楽だと思います。

ただ、本番環境では有効にすべき場合もあると思うので、設定ファイルとかで『コンテンツに基づく重複排除』ON時には MessageDeduplicationId を追加しないよう、挙動を切り替えられるように作っておくといいかも。(もちろんON時にメッセージがちゃんと遅れるかの単体テストも必要になりますが。)

テストプログラム

上記のモジュールのテストプログラムを作ってみたものが、以下のサンプルとなります。

テスト対象のモジュールを実行 → メッセージを受信 → メッセージボディのJSONをパース → メッセージ内容を検証 という流れになります。

なお、ここではメッセージ内容の細かい検証は行わず、expect().to.deep.equal() だけでやってしまってます。もっと細かい検証方法は基本編WebAPI編に書いてるので、よかったら読んでみてください。

'use strict';

const expect = require('chai').expect;
const aws = require('aws-sdk');
aws.config.update({
	region: 'ap-northeast-1',
});
const sqs = new aws.SQS();
const mymodule = require('../mymodule');

/** テスト対象のキューURL */
const STANDARD_QUEUE_URL = 'https://sqs.ap-northeast-1.amazonaws.com/999999999999/my-standard-queue';
const FIFO_QUEUE_URL = 'https://sqs.ap-northeast-1.amazonaws.com/999999999999/my-fifo-queue.fifo';


// itごとに行う準備
beforeEach(async function() {
	this.timeout(20000);
	
	// テスト対象のキューをカラにする
	await clearQueue(STANDARD_QUEUE_URL);
	await clearQueue(FIFO_QUEUE_URL);
});

// テスト本体
describe('SQS', function() {
	this.timeout(20000);
	
	it('sendMyMessageToStandardQueue() ... メッセージが正しい', async function() {
		// テスト対象モジュール実行
		await mymodule.sendMyMessageToStandardQueue();

		// ロングポーリングでメッセージ取り出し
		const messages = await receiveMessages(STANDARD_QUEUE_URL);

		// メッセージが1件だけであることを検証
		expect(messages).to.have.lengthOf(1);
		const message = messages[0];

		// メッセージボディを検証
		let body;
		try {
			body = JSON.parse(message.Body);
		} catch (e) {
			expect.fail('メッセージボディがJSON形式ではありません。');
		}
		expect(body).to.deep.equal({
			id: 552171000001,
			name: 'kiriukun',
			age: 14,
			memo: 'standard'
		});
	});
	
	it('sendMyMessageToFIFOQueue() ... メッセージが正しい', async function() {
		// テスト対象モジュール実行
		await mymodule.sendMyMessageToFIFOQueue();

		// 以下、キューが違うだけで同上
		const messages = await receiveMessages(FIFO_QUEUE_URL);

		expect(messages).to.have.lengthOf(1);
		const message = messages[0];

		let body;
		try {
			body = JSON.parse(message.Body);
		} catch (e) {
			expect.fail('メッセージボディがJSON形式ではありません。');
		}
		expect(body).to.deep.equal({
			id: 552171000001,
			name: 'kiriukun',
			age: 14,
			memo: 'fifo'
		});
	});
});

/**
 * キューのメッセージを全て削除する
 * @param  {String} queueUrl
 */
const clearQueue = async (queueUrl) => {
	// 一度に10件までしか受信できないので、それ以上入ってたらループして消す
	for (;;) {
		// 10件受信
		const res = await sqs.receiveMessage({
			QueueUrl: queueUrl,
			MaxNumberOfMessages: 10,
		}).promise();
		if (!res.Messages) {
			break;
		}
		// 全部削除
		for (let message of res.Messages) {
			await sqs.deleteMessage({
				QueueUrl: queueUrl,
				ReceiptHandle: message.ReceiptHandle,
			}).promise();
		}
	}
};

/**
 * メッセージを10件まで受信する
 * @param  {String} queueUrl
 * @return {Object[]} 受信したメッセージのリスト
 */
const receiveMessages = async (queueUrl) => {
	// 7秒ロングポーリングで10件受信
	const res = await sqs.receiveMessage({
		QueueUrl: queueUrl,
		MaxNumberOfMessages: 10,
		WaitTimeSeconds: 7,
	}).promise();

	if (!res.Messages) {
		// メッセージが無いとMessagesのキーごと無いので、カラ配列を返してやる
		return [];
	} else {
		// メッセージあったら消しておく
		for (let message of res.Messages) {
			await sqs.deleteMessage({
				QueueUrl: queueUrl,
				ReceiptHandle: message.ReceiptHandle,
			}).promise();
		}
	}

	return res.Messages;
};

実際に動かしてみる場合、キューのURLは適宜設定してください。これを mocha で実行すると、エラー無く終了するはずです。

このテストプログラムのポイントは、以下の4点です。

  • this.timeout() でタイムアウトを延ばす。ここでは20,000ミリ秒。mocha はデフォルトでは beforeEach()it() が2,000ミリ秒でタイムアウトするので、遅くなるかもしれないテストでは延ばしておく。
  • 検証時のメッセージの受信は、receiveMessage()WaitTimeSeconds を設定してロングポーリングで行う。ここでは7秒。
  • 検証時に受信したメッセージは、すぐに deleteMessage() で消す。
  • メッセージは想定される件数より多く (ここでは10件) 受信し、expect().lengthOf() で実際の件数に過不足が無いことを検証する。

ロングポーリングを使うと、WaitTimeSeconds 秒後までにキューに投入されたメッセージを受信できます。テスト対象のモジュールが送信したメッセージを実際に取り出せるようになるまではタイムラグがあるので、sleep() を自前で実装して待つより正確です。

受信したメッセージを即座に削除する理由ですが、メッセージを削除しない場合、検証のために受信したメッセージが『処理中』状態のまま残ってしまいます。『処理中』のメッセージは、キューの可視性タイムアウトが経過するまでは再度受信できません。なので、繰り返しテストプログラムを起動する場合にそのメッセージが beforeEach() の削除に引っかからず、予期せぬタイミングで戻ってきてしまうことを防ぐためです。

以上

以上でAmazon SQS編は終わりです。次はDynamoDBと思いましたが、これもう何やっても、テストの準備方法と取得方法が違うだけですね。でもたぶん書くと思います。