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

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

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

  1. npmでaws-sdkをインストール
  2. テーブルにテストデータを投入する方法
  3. テーブルからテストデータを消す方法 (1件)
  4. テーブルからテストデータを消す方法 (まとめて)
  5. 項目が存在し、想定通りの内容であることのテスト
  6. 項目が存在しないことのテスト
  7. 以上

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

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

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

npm install aws-sdk --save-dev

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

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

なお、この記事ではDynamoDBへのアクセスに AWS.DynamoDB.DocumentClient を使います。今のところ筆者は DocumentClient で不足を感じたことは無いのですが、AWS.DynamoDB.getItem() などで項目を取得する場合とは、データ型のチェック方法が異なってくると思います。あらかじめご了承ください。

テーブルにテストデータを投入する方法

項目の作成には DocumentClient.put() を使います。put() は、すでに同じプライマリキーの項目が存在する場合は上書きすることを留意してください。

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

const aws = require('aws-sdk');
const docClient = new aws.DynamoDB.DocumentClient({
	region: 'ap-northeast-1'  // 東京リージョン
});

before(async function() {
	this.timeout(20000);
	
	// 項目を作成
	await docClient.put({
		TableName: 'MyTable',
		Item: {
			MyId: 111,
			MyNumber: 12345,                           // Number型
			MyString: 'strrr',                         // String型
			MyBoolean: true,                           // Boolean型
			MyBinary: new Buffer([0x00, 0x01, 0x02]),  // Binary型
			MyNull: null                               // Null型
			MyList: [1,2,3],                           // List型 (中身はNumber型)
			MyMap: { a: 1, b: 2, c: 3 },               // Map型 (中身はNumber型)
		}
	}).promise();
});

アクセスに時間がかかった時のために、一応 this.timeout() でタイムアウト時間を延長しています。

また、DynamoDBにアクセスする DocumentClient の各メソッドは非同期的な関数ですが、.promise() を使うと await で同期的に呼び出せるようになります。

テストのときは同期的に呼び出せた方が解りやすいと思うので、この記事では基本的に await で呼ぶようにしてます。before() に渡してる関数を async にしてるのは、await を使うためです。

テーブルからテストデータを消す方法 (1件)

項目の削除には DocumentClient.delete() を使います。delete() は、存在しない項目に対して行ってもエラーにはなりません。

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

const aws = require('aws-sdk');
const docClient = new aws.DynamoDB.DocumentClient({
	region: 'ap-northeast-1'  // 東京リージョン
});

after(async function() {
	this.timeout(20000);
	
	// 項目を削除
	await docClient.delete({
		TableName: 'MyTable',
		Key: {
			MyId: 111,
		}
	}).promise();
});

テーブルからテストデータを消す方法 (まとめて)

あるパーティションキーの範囲内で全て消したい場合は、DocumentClient.query() で取得してから delete() で消します。ただし query() で一度に取得できる結果セットは1MBまでなので、確実に全件消すためには、ループ処理してやる必要があります。

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

const aws = require('aws-sdk');
const docClient = new aws.DynamoDB.DocumentClient({
	region: 'ap-northeast-1'  // 東京リージョン
});

before(async function() {
	this.timeout(20000);
	
	const tableName = 'MyTable';
	const targetPartitionKey = 'hogehoge';
	let exclusiveStartKey = null;
	for (;;) {
		// まとめてプライマリキーを取得
		const res = await docClient.query({
			TableName: tableName,
			KeyConditionExpression: '#MyPartitionKey = :MyPartitionKey',
			ExpressionAttributeNames:{
				'#MyPartitionKey': 'MyPartitionKey'
			},
			ExpressionAttributeValues:{
				':MyPartitionKey': targetPartitionKey
			},
			AttributesToGet: ['MyPartitionKey', 'MySortKey'],  // プライマリキー属性のみ取得する
			ExclusiveStartKey: exclusiveStartKey,              // ExclusiveStartKey以降を取得する
		}).promise();
		
		// プライマリキーで項目を削除
		if (res.Items) {
			for (let item of res.Items) {
				await docClient.delete({	
					TableName: tableName,
					Key: {
						MyPartitionKey: item.MyPartitionKey,
						MySortKey: item.MySortKey,
					}
				}).promise();
			}
		}
		// 読んでないデータが無ければ抜ける、残っていれば続ける
		if (res.LastEvaluatedKey === undefined) {
			break;
		}
		exclusiveStartKey = res.LastEvaluatedKey;
	}
});

もしパーティションキーすら不明だとか、全てのデータを消したいとかなら、query() の代わりに DocumentClient.scan() を使ってください。

scan()
const res = await docClient.scan({
	TableName: tableName,
	AttributesToGet: ['MyPartitionKey', 'MySortKey'],
	ExclusiveStartKey: exclusiveStartKey,
}).promise();

項目が存在し、想定通りの内容であることのテスト

項目の取得には DocumentClient.get() を使います。

ただしこの方法だと、プライマリキーがあらかじめ判っている項目でないと取得できません。もしプライマリキーが判らない項目をテストしなければならない場合は、scan() とかを使って走査するしかないと思います。

以下のサンプルでは、テスト対象のモジュールを実行した体で適当なデータを作成し、それを改めて取得して検証しています。

const expect = require('chai').expect;
const aws = require('aws-sdk');
const docClient = new aws.DynamoDB.DocumentClient({
	region: 'ap-northeast-1'
});

describe('DynamoDB', function() {
	this.timeout(20000);
	
	it('項目が存在し、想定通りの内容である', async function() {
		// ※テスト対象モジュールを実行した体で項目を作成
		await docClient.put({
			TableName: 'MyTable',
			Item: {
				MyId: 111,
				MyNumber: 12345,
				MyString: 'strrr',
				MyBoolean: true,
				MyBinary: new Buffer([0x00, 0x01, 0x02]),
				MyNull: null,
				MyList: [1,2,3],
				MyMap: { a: 1, b: 2, c: 3 },
			}
		}).promise();
		
		// 作成された項目を取得
		const res = await docClient.get({
			TableName: 'MyTable',
			Key: {
				MyId: 111,
			},
			ConsistentRead: true  // 確実に最新のデータを取得する (強力な整合性のある読み込み)
		}).promise();

		// 項目が存在する場合、戻りのオブジェクトにItemキーがあるはず
		expect(res).to.include.all.keys('Item');
		
		// 項目の内容を検証
		expect(res.Item).to.deep.equal({
			MyId: 111,
			MyNumber: 12345,
			MyString: 'strrr',
			MyBoolean: true,
			MyBinary: new Buffer([0x00, 0x01, 0x02]),
			MyNull: null,
			MyList: [1,2,3],
			MyMap: { a: 1, b: 2, c: 3 },
		});
	});
});

ここでは DocumentClient.get() で項目を取得するときに、ConsistentRead: true を指定しています。これを使うとDynamoDBの読み込みコストが2倍かかりますが、確実に最新のデータを検証できるように、テストプログラムでは常に指定した方が良いと私は思っています。

読み込み整合性については、詳しくは以下の公式ドキュメントを参照してください。

また、上記のサンプルでは項目の内容を expect().to.deep.equal() でいっぺんに検証してます。ひとつずつ細かく検証する場合は、私なら以下のようにアサーションすると思います。

// 項目が存在する場合、戻りのオブジェクトにItemキーがあるはず
expect(res).to.include.all.keys('Item');
const item = res.Item;

// MyId は数値型で、値が 111 であるか
expect(item.MyId).to.equal(111);

// MyNumber は数値型で、値が 12345 であるか
expect(item.MyNumber).to.equal(12345);

// MyString は文字列型で、値が strrr であるか
expect(item.MyString).to.equal('strrr');

// MyBoolean は真偽値で、値が true であるか
expect(item.MyBoolean).to.be.true;

// MyBinary はBuffer型で、中身が 0x00, 0x01, 0x02 であるか
expect(item.MyBinary).to.deep.equal(new Buffer([0x00, 0x01, 0x02]));

// MyNull は null であるか
expect(item.MyNull).to.be.null;

// MyList は配列で、中身が 1, 2, 3 であるか
expect(item.MyList).to.deep.equal([1,2,3]);

// MyMap はオブジェクトで、中身が a: 1, b: 2, c: 3 であるか
expect(item.MyMap).to.deep.equal({ a: 1, b: 2, c: 3 });

// キーに過不足は無いか
expect(item).to.have.all.keys('Id', 'MyNumber', 'MyString', 'MyBoolean', 'MyBinary', 'MyNull', 'MyList', 'MyMap');

配列・オブジェクト・Buffer は、.deep.equal() を使うのが一番楽です。型までちゃんと見てくれます。

ひとつずつ値を検証する場合、漏れなく全てのキーを検証できているか、.have.all.keys() で最後に確認するのが良いと思います。最初に確認してしまうと、具体的な値をまったく検証していないので、ほんとに「キーに過不足があった」ことしか分からないからです。

項目が存在しないことのテスト

DocumentClient.get() で項目を取得し、戻りに Item キーが含まれていないことを確認するだけです。

const expect = require('chai').expect;
const aws = require('aws-sdk');
const docClient = new aws.DynamoDB.DocumentClient({
	region: 'ap-northeast-1'
});

describe('DynamoDB', function() {
	this.timeout(20000);
	
	it('項目が存在しない', async function() {
		// 存在しない項目を取得
		const res = await docClient.get({
			TableName: 'MyTable',
			Key: {
				Id: 999,
			},
			ConsistentRead: true  // 確実に最新のデータを取得する (強力な整合性のある読み込み)
		}).promise();

		// 項目が存在しない場合、戻りのオブジェクトにItemキーは含まれてないはず
		expect(res).to.not.include.keys('Item');
	});
});

以上

以上でAmazon DynamoDB編は終わりです。今のところ筆者は、DynamoDBを使ったアプリのテストプログラムは、この記事の内容だけでそこそこ不自由なく書けています。でも、プライマリキーが分からない項目のテストは、もうちょっとどうにかならないのかなとは思います。

次回はブラウザ用JavaScript編です。