はじめに
皆さん、こんにちは。AWS DevTools Hero の後藤と申します。普段、AWS Cloud Development Kit (AWS CDK) へのコントリビュート活動を行っており、Top Contributor、並びに Community Reviewer に選定いただいています。
AWS CDK は、使い慣れたプログラミング言語でクラウドリソースを定義できるオープンソースのフレームワークです。プログラミング言語で書けるということは、一般的なソフトウェア開発で行われるようなテストコードを書くことができるということです。
今回はそんな IaC ツールである AWS CDK におけるテストのうち、単体テストの使い所、つまり「どんな場面でどのように単体テストを使えば良いのか」について紹介します。
builders.flash メールメンバー登録
1. 単体テストとは
一般的なソフトウェア開発における単体テスト (Unit Tests) とは、関数やクラスなど、アプリケーションの最小の構成要素である「ユニット」に対して行われるテストのことです。
単体テスト以外にも、単体テストよりも広い範囲を対象とする統合テスト (Integration Tests) などのテストもあります。
それらと比べると、単体テストはユニット単位の小さい粒度を対象としたテストであるため、バグの検出時に原因を特定しやすく、またテストの実行が速いというメリットがあります。
2. AWS CDK における単体テストの種類
AWS CDK における単体テストの種類には、主に以下の 3 つがあります。
- スナップショットテスト
- Fine-grained assertions テスト
- バリデーションテスト
前者 2 つに関しては、AWS CDK の Developer Guide の Test AWS CDK applications というページに詳しい説明が掲載されているので、ぜひご覧ください。また、AWS Black Belt Online Seminar にて公開されている AWS CDK における開発とテスト (Advanced #1) からもこれらのテストについて学ぶことができます。
また、AWS CDK における統合テストは、実際に CDK コードで定義したリソースをデプロイして動作確認するのが一般的で非常に有用なテストなのですが、本記事では統合テストに関しては触れません。AWS CDK における統合テストに関しては、AWS の公式ブログで AWS CDK アプリケーションのためのインテグレーションテストの作成と実行 という記事が公開されているのでぜひご覧ください。
ではここから、AWS CDK におけるそれぞれの単体テストの書き方や、具体的にどんな場面でどのように使えば良いのかといった使い所についてご紹介していきます。
3. スナップショットテスト
3-1. スナップショットテスト
AWS CDK におけるスナップショットテストとは、CDK コードから合成される AWS CloudFormation テンプレートを出力し、以前のテスト実行時に生成したテンプレートの内容と比較してテンプレートの差分を検出するテストのことです。
例えば、MyStack という Stack クラスに対するスナップショットテストは、以下のような書き方で行うことができます。
※テストファイルは、cdk initコマンドで CDK プロジェクト作成時にプロジェクトのルートディレクトリの直下に test というディレクトリが作られているため、その中に my-stack.test.ts というようなファイルを作成して書くことが一般的です。

テストファイルの実行
このテストファイルを、cdk init を実行した際に定義されている npm run test コマンドで実行すると、test ディレクトリの直下に __snapshots__ というディレクトリが作成され、その中に my-stack.test.ts.snap というスナップショットファイルが保存されます。スナップショットファイルは、CloudFormation テンプレートの JSON 形式で保存されています。そして、その以前のテスト実行時に保存されたスナップショットと、今回の CDK コードによって生成されたテンプレートを比較し、差分がある場合はテストが失敗します。
その差分が想定通りのものである場合、npx jest --updateSnapshotというコマンドでテストを実行することで、スナップショットファイルを今回生成されたものに更新することができます。
3-2. スナップショットテストの使い所
4. Fine-grained assertions テスト
4-1.Fine-grained assertions テストとは
AWS CDK における Fine-grained assertions テストとは、生成された CloudFormation テンプレートの一部を取り出して、その部分に対してチェックを行うテストのことです。これにより、どのようなリソースが生成されるのかといった細かい構成要素に対するテストをすることができます。
例えば、「AWS::SNS::Subscription のリソースが 2 つ生成されているか」や、「AWS::Lambda::Function の Runtime に nodejs20.x が設定されているか」といったような細かい粒度のテストを行うことができます。

テストコードの例

4-2. Fine-grained assertions テストの使い所
この Fine-grained assertions テストですが、実は具体的にどういう時にどんなテストを書けば良いのか、といったセオリーはまだあまり確立されていないように感じます。
というのも、リソースごと、プロパティごとといった細かい粒度で様々なチェックをすることができ、多岐にわたるテストケースが考えられるためです。また全てのリソースに対してテストを書いていくとさらに膨大な量になり、テストの旨みよりも冗長さやメンテナンスの大変さが上回ってしまうケースもあります。
そのため、あくまで私個人の判断基準となりますが、以下のような場面で使うのが良いと考えています。
1. ループ処理
2. 条件分岐
3. プロパティの override
4. 特に保証したい定義
5. props を使った値の指定
前提として、AWS CDK はプログラミング言語で書けるが故に「手続き的」にリソース定義のコード記述を行うこともできますが、「宣言的」にリソース定義の記述を行うことが可能です。
※ここでいう「宣言的」とは、「○○ というリソースを作成する」というように、リソースの存在を宣言することを指します。一方で「手続き的」とは、「○○ というリソースを作成するために、まずはこういう処理を行い、次にこういう処理を行う」といったように、リソースの作成手順を手続き的に記述することを指します。
インフラ定義としては、やはり定義を見るだけでどんなリソースが生成されるのかがわかりやすい「宣言的」な方が好ましいケースが多いかと思われます。AWS CDK はあくまでインフラ定義のためのツールであるため、基本的には「宣言的」に書くことが多いでしょう。
そして、「リソース A を作る」というような宣言的な記述において、「リソース A が作られる」というのは自明です。自明なものを確認する旨みに対して、Fine-grained assertions テストではリソース定義側のコードとほぼ同じようなコードが出来上がることで二重定義のような煩わしさを感じ、テストのメンテナンスコストが上がってしまうこともあります。
そのような理由から、私の個人的な判断基準としては、全てのリソースに対して細かく Fine-grained assertions テストを書くのではなく、上記のような場面で使うのが良いと考えています。
4-2-1. ループ処理
上記では「宣言的」にリソース定義の記述を行うお話をしましたが、ループ処理を使ってリソースを生成する場合、それは「手続き的」なコード記述になります。
つまり、これによってどのようなリソースが生成されるかは自明とは言えなくなってしまいます。
そのためループ処理を使ってリソースを生成する場合には、そのループ処理が正しく動作しているかを確認するための Fine-grained assertions テストを書くことが重要になります。
ループ処理を使ったリソース定義
具体的には、以下のような CDK コードがあるとします。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Topic } from 'aws-cdk-lib/aws-sns';
export interface MyStackProps extends cdk.StackProps {
appNames: string[];
}
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: MyStackProps) {
super(scope, id, props);
// 重複する要素がある場合を考慮して一意な組み合わせにする
const appNames = new Set(props.appNames);
for (const appName of appNames) {
new Topic(this, `${appName}Topic`, {
displayName: `${appName}Topic`,
});
}
}
}
Fine-grained assertions

4-2-2. 条件分岐
if 文のような条件分岐を使って環境ごとにリソースを生成するかどうかを変えるような場合も、その条件分岐が正しく動作しているかの確認は重要です。 以下のような CDK コードがあるとします。
import * as cdk from 'aws-cdk-lib';
import { CfnWebACL } from 'aws-cdk-lib/aws-waf';
import { Construct } from 'constructs';
export interface MyStackProps extends cdk.StackProps {
isProd: boolean;
}
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: MyStackProps) {
super(scope, id, props);
if (props.isProd) {
new CfnWebACL(this, 'WebAcl', {
// ...
});
}
}
}
テストの例

環境ごとにプロパティを指定するかどうかを変えるような場合
今度は、環境ごとにプロパティを指定するかどうかを変えるような場合です。
import * as cdk from 'aws-cdk-lib';
import { Distribution } from 'aws-cdk-lib/aws-cloudfront';
import { Construct } from 'constructs';
export interface MyStackProps extends cdk.StackProps {
isProd: boolean;
}
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: MyStackProps) {
super(scope, id, props);
// ...
new Distribution(this, 'Distribution', {
// ...
webAclId: props.isProd ? webAclId : undefined,
});
}
}
Match.absent

4-2-3. プロパティの override
AWS CDK では、CDK で提供される L2 Construct を使ってリソースを定義することが一般的です。
しかし、L2 Construct には対応していないプロパティを設定したいケースなどで、エスケープハッチ を用いて L1 Construct にキャストしてから、addPropertyOverride などのメソッドでプロパティを override (上書き) することがあります。
この場合プロパティの指定に Construct の型が使えず、CloudFormation テンプレートの構造に合わせて自前でプロパティの記述をする必要があり、特に階層構造のようなプロパティの場合に記述ミスが発生しやすいです。
エスケープハッチ
コード
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Bucket, CfnBucket } from 'aws-cdk-lib/aws-s3';
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
const bucket = new Bucket(this, 'Bucket');
// Bucket をエスケープハッチしてプロパティを override する
const cfnSrcBucket = bucket.node.defaultChild as CfnBucket;
cfnSrcBucket.addPropertyOverride('NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled', true);
}
}
テストの例

4-2-4. 特に保証したい定義
次は、特に保証したい定義に対してテストを書くケースです。
これは最初にご説明した「宣言的」なコード記述に対して行うテストになります。
先ほどは、「宣言的」、つまりリソース定義が自明なものには Fine-grained assertions テストを書かないかのような説明をしましたが、特に保証したい定義に対してテストを書くことも重要です。
例えば、「ある要件を実現するためにこのプロパティは設定しておきたい」などのような、他のプロパティと比べて重要な定義に対して、「意思表示」 のような形でテストを書いておくことができます。もし今後、別の開発者がその設定を変更してしまった際に該当するテストが失敗し、その違反したテストを参照することで元の設計者の意図を理解でき、意思の伝搬に繋がることもあります。
ライフサイクルで期限設定をする
コード
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Bucket } from 'aws-cdk-lib/aws-s3';
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
new Bucket(this, 'Bucket', {
lifecycleRules: [{ expiration: cdk.Duration.days(100) }],
});
}
}
テストコードの例

Match.anyValue
この場合、開発中の要件変更などでプロパティの値を変更した際に、テスト側の値も合わせて変更する必要があります。もしプロパティを指定できているかだけを確認できれば良い場合は、Match.anyValue メソッドを用いることで具体値の指定はせずとも確認することができ、テストのメンテナンスコストを下げることができます。
import { Match } from 'aws-cdk-lib/assertions';
// ...
template.hasResourceProperties('AWS::S3::Bucket', {
LifecycleConfiguration: {
Rules: [
{
ExpirationInDays: Match.anyValue(),
Status: 'Enabled',
},
],
},
});
addDependency
また、例えば addDependency のような CDK によって提供されるメソッドを使って定義を加える際にも、意図した通りに反映されていることを保証したいケースもあるでしょう。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { LogGroup, ResourcePolicy } from 'aws-cdk-lib/aws-logs';
import { PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { HostedZone } from 'aws-cdk-lib/aws-route53';
import { Bucket, CfnBucket } from 'aws-cdk-lib/aws-s3';
export interface MyStackProps extends cdk.StackProps {
domainName: string;
}
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: MyStackProps) {
super(scope, id, props);
const logGroup = new LogGroup(this, 'QueryLogGroup');
const hostedZone = new HostedZone(this, 'HostedZone', {
zoneName: props.domainName,
queryLogsLogGroupArn: logGroup.logGroupArn,
});
const resourcePolicy = new ResourcePolicy(this, 'QueryLogResourcePolicy', {
policyStatements: [
new PolicyStatement({
principals: [new ServicePrincipal('route53.amazonaws.com')],
actions: ['logs:CreateLogStream', 'logs:PutLogEvents'],
resources: [logGroup.logGroupArn],
}),
],
});
// HostedZone が QueryLogResourcePolicy に依存するように
hostedZone.node.addDependency(resourcePolicy);
}
}
テストの例

4-2-5. props を使った値の指定

実際の Stack に渡す用に定義した props をそのまま使う場合

5. バリデーションテスト
5-1. バリデーションテストとは
3 つ目にご説明するバリデーションテストとは、その名の通りバリデーションに関するテストになります。
バリデーションとは、条件分岐などを通して値の妥当性を検証する処理のことです。AWS CDK においても、Stack や Construct への入力である props のプロパティに対してバリデーション処理を実装することがあります。
AWS CDK における具体的なバリデーションの方法に関しては、筆者が以前 builders.flash で執筆した AWS CDK におけるバリデーションの使い分け方を学ぶ という記事をご覧ください。
入力値を検証するバリデーションテストの例
例えばバリデーションテストには、あるプロパティに対する入力値が特定の範囲内に収まっているかを検証するコードを書くことができます。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Bucket } from 'aws-cdk-lib/aws-s3';
export interface MyStackProps extends cdk.StackProps {
lifecycleDays: number;
}
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: MyStackProps) {
super(scope, id, props);
if (!cdk.Token.isUnresolved(props.lifecycleDays) && props.lifecycleDays > 400) {
throw new Error('ライフサイクル日数は400日以下にしてください');
}
new Bucket(this, 'Bucket', {
lifecycleRules: [
{
expiration: cdk.Duration.days(props.lifecycleDays),
},
],
});
}
}
cdk.Token.isUnresolved
※ cdk.Token.isUnresolved メソッドは、値が Token でないかどうかを確認するメソッドになります。こちらの解説なども上記の「AWS CDK におけるバリデーションの使い分け方を学ぶ」記事に記載しているため、ぜひご覧ください。
テストの例

5-2. バリデーションテストの使い所
Stack や Construct が受け取るプロパティに対して何らかのバリデーション処理を実装している場合、そのバリデーション処理が正しく動作しているかは非常に重要な確認事項であるため、バリデーションテストはぜひ書いておきたいテストです。各バリデーションごとにテストケースを書けると良いでしょう。
逆に言えば、特にバリデーション処理を何も実装していない場合は不要となります。
6. CDK の単体テストで覚えておくと良いこと
6-1. 個数チェックと自動生成リソース
Fine-grained assertions テストでは、assertions モジュール の Template クラスが持つ resourceCountIs メソッドなどで、特定のリソースタイプの個数を確認するテストを書くことができます。
const template = Template.fromStack(stack); template.resourceCountIs('AWS::Logs::LogGroup', 5);
L2 Construct
一方で、CDK でよく使う L2 Construct ですが、ベストプラクティスに沿ったり、より高い開発者体験を提供するために、Construct 内部に自動でいくつかのリソースが生成されることがあります。そこで、例えば上記のテストのように、自分ではその種類のリソースを 5 つ定義したつもりでも、実際には 6 つのリソースが生成されていた、といったようなケースがあります。
また、そのようなケースも加味してテストで指定する個数を 6 つとしたとしても、後からその値を見た際に内訳がよくわからず混乱や認知負荷につながることもあるため注意しましょう。よほど自動生成リソースも含めた個数を確認したいわけではない場合、リソースの自動生成はスナップショットテストの更新差分からでも確認可能なため、このような個数チェックを捨てるといった選択肢に目を向けるのも良いかもしれません。それでもテストを残したい場合、意図がわかるようにきちんとコメントを書くことも良いでしょう。
resourcePropertiesCountIs メソッド
もしくは、resourcePropertiesCountIs メソッドを使用して、特定のプロパティや値を持つリソースに絞った個数を確認するテストもぜひご検討ください。
const template = Template.fromStack(stack);
template.resourcePropertiesCountIs(
'AWS::Logs::LogGroup',
{
// '/aws/lambda/my-app/'という命名規則を持つロググループに限定
LogGroupName: Match.stringLikeRegexp('/aws/lambda/my-app/'),
},
5,
);
6-2. Construct ごとのテスト
本記事では基本的に、実際に定義した Stack クラスに対する単体テストを例としてご紹介しました。
しかし CDK コードを書く上で、カスタム Construct を作成して、それらを組み合わせて Stack を定義することも多いかと思います。
そのような場合、カスタム Construct ごとに単体テストを書くことで、その Construct に閉じた範囲でのテストを行うことができ、他の Construct に影響を受けずに動作を確認することができます。これにより、Construct 単体での信頼性や再利用性を担保することができます。
また、テストファイルを Construct ごとに分けることで、一つ一つのテストファイルが責務ごとに凝集されてよりシンプルになり、理解容易性が増すかもしれません。
テストの例

注意すべき点
しかし、Construct の数が多くなるにつれ、全ての Construct ごとにテストを書こうとするとテストの数が非常に多くなってしまうことがあります。また、Construct ごとのテストを書いたとしても、実際にデプロイされる環境の構成となる Stack に対するテストは必須で書いておきたいでしょう。その場合、Stack のテストと Construct のテストで重複しないようにうまくテストの範囲や責務を分けないと、テストのメンテナンスが大変になる可能性があるため注意が必要です。
再利用性を特に担保したい Construct のみに Construct 単位のテストを書くといった使い分けも良いでしょう。もしくは、特に Construct を再利用するケースがない場合は、Construct ごとの単体テストを書かないという選択肢も問題ないと思います。
7. オススメの最小構成
本記事でご紹介した全てのテストをまとめて導入することは中々大変かと思われます。
そのため最小構成でお手軽に CDK での単体テストを導入したい場合、まずはスナップショットテストから導入してみるのが良いでしょう。簡単に導入ができ、かつデプロイされる CloudFormation テンプレートでの思わぬ変更を検知できることは非常に大きなメリットです。
8. まとめ
AWS CDK における単体テストとして、以下の 3 つの単体テストの書き方、およびそれらの使い所についてご紹介しました。
- スナップショットテスト
- Fine-grained assertions テスト
- バリデーションテスト
単体テストは AWS CDK においても信頼性を向上するために非常に重要な要素です。ぜひこれらのテストを活用して、より信頼性の高い AWS CDK 開発に取り組んでみてもらえると幸いです。
筆者プロフィール
後藤 健太 (AWS DevTools Hero / @365_step_tech)
AWS CDK のコントリビュート活動を行っており、Top Contributor や Community Reviewer に選定。2024 年 2 月に発足されたコミュニティ駆動の CDK コンストラクトライブラリである Open Constructs Library では、メンテナーを担っている。
また、cls3 や delstack といった自作 AWS ツールの OSS 開発も行なっている。2024 年 3 月、AWS DevTools Hero に選出。

Did you find what you were looking for today?
Let us know so we can improve the quality of the content on our pages