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

今回は、AWSのSQSを扱うモジュールの単体テストをMochaで自動化する方法についてです。

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

  1. FIFOキューの『コンテンツに基づく重複排除』について
  2. npmでaws-sdkをインストール
  3. キューにメッセージを投入する方法
  4. キューをカラにする方法
  5. メッセージが投入されており、想定通りの内容であることのテスト
  6. キューがカラであることのテスト
  7. 以上

FIFOキューの『コンテンツに基づく重複排除』について

SQSが絡むテストを自動化する場合、テストプログラムを作る前に以下を確認してください。

  • テスト対象のモジュールが使っているSQSキューはFIFOキューである。
  • そのFIFOキューは『コンテンツに基づく重複排除』がONである。
  • 自動化しようとしている複数のテストケースの中で、完全に同じメッセージが送信されるケースが2つ以上存在する。

上記全てに該当する場合、テストプログラムを作っても想定通りに動かない可能性が高いです。

『コンテンツに基づく重複排除』がONのFIFOキューは、内容が全く同じメッセージを5分以内に受信しても、最初の1件しか配信しないからです。なので短時間中に本文が同じメッセージを複数回送るようなテストプログラムは、2件目以降が空振りしてしまいます。

どうしてもテスト自動化したい場合は、全ケースで必ず異なるメッセージが送信されるようにテストケースを作るとか、そもそも『コンテンツに基づく重複排除』を使う必要があるのか検討するとか、別方面での工夫が必要になります。

(テスト中だけ『コンテンツに基づく重複排除』をOFFにしようとしても、『コンテンツに基づく重複排除』をOFFにすると、今度はメッセージ送信時にパラメータで『重複排除ID (MessageDeduplicationId)』を指定しないといけなくなります。テスト対象モジュールがそれに対応した作りになっていない限り、これも無理です。)

では、次項から本題です。

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

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

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

npm install aws-sdk --save-dev

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

また、aws-sdkを利用するには開発環境にAWSの認証情報を設定する必要があるので、設定してない場合は設定します。方法は以下の公式マニュアルの『ステップ 1: 認証情報を設定する』を参照してください。

キューにメッセージを投入する方法

初期データとして、テストプログラムからキューにメッセージを投入したいことがあります。メッセージの投入には SQS.sendMessage() を使います。

投入先のキューがFIFOキューの場合、パラメータにグループID (MessageGroupId) が必要です。また、FIFOキューの中でも『コンテンツに基づく重複排除』がONの場合、さらに重複排除ID (MessageDeduplicationId) が必要になります。テスト対象のモジュールが使うキューに合わせて設定してください。

例えば before() で行うなら、以下のようにします。

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

before(async function() {
	this.timeout(20000);
	
	// スタンダードキューにメッセージ投入
	await sqs.sendMessage({
		QueueUrl: 'https://sqs.ap-northeast-1.amazonaws.com/999999999999/my-standard-queue',
		MessageBody: JSON.stringify({
			id: 111,
			name: 'kiriukun',
			age: 14
		}),
	}).promise();

	// FIFOキュー (『コンテンツに基づく重複排除』OFF) にメッセージ投入
	await sqs.sendMessage({
		QueueUrl: 'https://sqs.ap-northeast-1.amazonaws.com/999999999999/my-fifo-queue.fifo',
		MessageBody: JSON.stringify({
			id: 111,
			name: 'kiriukun',
			age: 14
		}),
		MessageGroupId: 'my-group'
	}).promise();

	// FIFOキュー (『コンテンツに基づく重複排除』ON) にメッセージ投入
	// MessageDeduplicationId は絶対に重複しないように適当な値を設定
	await sqs.sendMessage({
		QueueUrl: 'https://sqs.ap-northeast-1.amazonaws.com/999999999999/my-deduplication-fifo-queue.fifo',
		MessageBody: JSON.stringify({
			id: 111,
			name: 'kiriukun',
			age: 14
		}),
		MessageGroupId: 'my-group',
		MessageDeduplicationId: String((new Date()).getTime() + Math.floor(Math.random() * 100000))
	}).promise();
});

キューをカラにする方法

テスト前など、キューをカラにしたいことがよくあります。

キューをカラにするメソッドとしては SQS.purgeQueue() というのがありますが、これは削除に最大60秒かかるらしいので、使ったことありません。私は単純に SQS.receiveMessage() で10件ずつ取り出して、全部 SQS.deleteMessage() で消してます。

例えば before() で行うなら、以下のようにします。

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

before(async function() {
	this.timeout(20000);

	// 一度に10件までしか取得できないので、カラになるまでループする
	const queueUrl = 'https://sqs.ap-northeast-1.amazonaws.com/999999999999/my-standard-queue';
	for (;;) {
		// 10件取得
		const res = await sqs.receiveMessage({
			QueueUrl: queueUrl,
			MaxNumberOfMessages: 10,
		}).promise();
		if (!res.Messages) {
			break;
		}
		// 1件ずつ削除
		for (let message of res.Messages) {
			await sqs.deleteMessage({
				QueueUrl: queueUrl,
				ReceiptHandle: message.ReceiptHandle,
			}).promise();
		}
	}
});

また、これも私は使ったことありませんが、SQS.deleteMessageBatch() というのもあるので、そっちを使えばもっとリクエスト回数を減らせるかもしれないです。

メッセージが投入されており、想定通りの内容であることのテスト

メッセージの取得には SQS.receiveMessage() を使います。

取得する際、メッセージの件数が想定通りであることを確認するために、パラメータの MaxNumberOfMessages を想定されるメッセージ件数より多く設定します。キューに投入した後すぐに取り出そうとすると空振りすることがあるので、WaitTimeSeconds も設定してロングポーリングさせると確実です。

それから、メッセージの内容の正しい正しくないに関わらず、受信したメッセージは即座に削除します。メッセージを削除しない場合、テストプログラム内で取得したメッセージが『処理中』状態のまま残り、可視性タイムアウトが経過した後でキューに戻ってきてしまうからです。

以下のサンプルでは、テスト対象のモジュールを実行した体でキューに適当なメッセージを投入し、改めてそれを取得して検証しています。

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

describe('SQS', function() {
	this.timeout(20000);
	
	it('メッセージが投入されており、想定通りの内容である', async function() {
		const queueUrl = 'https://sqs.ap-northeast-1.amazonaws.com/999999999999/my-standard-queue';

		// ※テスト対象モジュールを実行した体でメッセージを投入
		await sqs.sendMessage({
			QueueUrl: queueUrl,
			MessageBody: JSON.stringify({
				id: 111,
				name: 'kiriukun',
				age: 14
			}),
		}).promise();

		// 7秒ロングポーリングでメッセージ10件取得
		const res = await sqs.receiveMessage({
			QueueUrl: queueUrl,
			MaxNumberOfMessages: 10,
			WaitTimeSeconds: 7,
		}).promise();

		// メッセージがある場合、戻りのオブジェクトにMessagesキーがあるはず
		expect(res).to.include.all.keys('Messages');

		// メッセージがあれば全てキューから消しておく
		for (let message of res.Messages) {
			await sqs.deleteMessage({
				QueueUrl: queueUrl,
				ReceiptHandle: message.ReceiptHandle,
			}).promise();
		}

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

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

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

キューがカラであることのテスト

SQS.receiveMessage() で取得し、戻りに Messages キーが含まれていないことを確認するだけです。

この場合も、もしメッセージが取得できてしまったら必ず消しておきます。消す前に内容をコンソールに出力しておくと、何が誤って送信されてしまったのか分かって便利です。

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

describe('SQS', function() {
	this.timeout(20000);
	
	it('キューがカラである', async function() {
		const queueUrl = 'https://sqs.ap-northeast-1.amazonaws.com/999999999999/my-standard-queue';

		// 7秒ロングポーリングでメッセージ10件取得
		const res = await sqs.receiveMessage({
			QueueUrl: queueUrl,
			MaxNumberOfMessages: 10,
			WaitTimeSeconds: 7,
		}).promise();

		// キューがカラの場合、戻りのオブジェクトにMessagesキーは含まれてないはず
		try {
			expect(res).to.not.include.all.keys('Messages');
		} catch (e) {
			// メッセージがあれば全てキューから消しておく
			console.log('メッセージ ->', JSON.stringify(res.Messages, null, 4));
			for (let message of res.Messages) {
				await sqs.deleteMessage({
					QueueUrl: queueUrl,
					ReceiptHandle: message.ReceiptHandle,
				}).promise();
			}
			throw e;
		}
	});
});

以上

以上でAmazon SQS編は終わりです。次はAWS DynamoDB編です。これもう何やっても、データ投入方法と取得方法が違うだけですね。でもたぶん書くと思います。