TOP>コラム一覧>AWS CDK 入門 - アサーションテストの基本

AWS CDK 入門 - アサーションテストの基本

はじめに

本記事は CDK アサーションテストの入門記事です。
CDK のアサーションテストに興味があるが、始め方が分からない方向けに、アサーションテストの基本的な内容を整理しました。

前提

本記事は以下を使用する前提で記載しています。

  • Typescript: 5.2.2
  • Jest: 29.7.0
  • CDK: 2.128.0

CDK におけるユニットテスト / アサーションテストとは?

まず、CDK ではどのようにユニットテストを行うのか説明します。

CDK でユニットテストを行う際は、アプリの開発と同様に、各言語で使用される一般的なテスティングフレームワーク(Typescript で言えば jest 等)を使用してテストコードを実装できます。

また、公式から CDK のテストを支援するためのモジュールが提供されているため、通常、これらを使用してテストコードを書きます。

テストモジュール 説明
アサーションテスト
(Fine-GrainedAssertion Test)
  • CDK スタックから合成した CloudFromation テンプレートに対して、リソースが特定のプロパティを持つかテストするために使用する。
  • 作成しておくことで、改修やリファクタリングの際の回帰テストに役立つ。
スナップショットテスト
(Snapshot Test)
  • CDK スタックから合成した CloudFormation テンプレートに対して、以前保存したテンプレートとの差分を比較する。
  • リファクタリングの際に構成の変更がないことを確認する場合や軽微なパラメータ変更の差分を確認するために使用する。

アサーションテストは伝統的なインフラ構築における、単体テスト(パラメータ確認)に相当するイメージです。
スナップショットテストはコードを修正した際に、テンプレートが構成するリソースの内容の更新箇所を把握するために使用します。

因みに、上記はいずれも AWS 環境ではなく、コードに対してテストを行います。 余談にはなりますが、リソースを作成して、テストしたい場合は、CDK integ-tests / integ-rnnner があります。(現時点では、アルファ版の公開)

CDK アサーションテストの基本解説

CDK Assertion test モジュールの基本的な説明をします。

CDK アサーションテストの基本クラス

Template

CDK スタックのアサーションテストスイートになります。
簡単に言うと、Template に CDK スタックを入力し、Template を作成すると、スタックから合成されたCloudFormation テンプレートに対する各種テストができるようになります。

テンプレートクラスでは、例えば以下のようなテスト用のメソッドが提供されています。

  • 特定のサービスのリソース数量をカウントする: resourceCountIs()
  • 特定のサービスにおける、指定のプロパティを持つリソースをカウントする。: resourcePropertiesCountIs()
  • 特定のサービスにおける、指定のプロパティを持つリソースが存在するか確認する。: hasResourceProperties()

CDK API リファレンス / aws-cdk-lib.assertions.Template

Match

パラメータの一致条件です。例えば、以下のようなものがあります。

  • 非 Null のいずれかの値に一致: Match.anyValue ()
  • 配列の部分一致: Match.arrayWith ()
  • 配列の正確な一致: Match.arrayEquals ()

マッチャーのメソッド全容は以下の AWS CDK の API リファレンスを参照してください。

CDK API リファレンス / aws-cdk-lib.assertions.Match

CDKのアサーションテストでは、CloudFromation のリソースプロパティを指定する事でテストします。その為、CloudFromation のリソースタイプを調べる必要があります。

アサーションテストを書く際に使うリンク集

アサーションテストをやってみる

本セクションでは、CDK のアサーションテストを実際にやってみます。

サンプルプログラム の準備

以降では、AWS ソリューションコンストラクトのaws-cloudfront-s3 をテスト対象として、テストコードを記載しています。

AWS Solutions Constructs / aws-cloudfront-s3

AWS Solution コンストラクトは AWS が提供しているCDKのテンプレートです。

CDK アプリの準備

以下を実行して、CDK アプリのテンプレートを作成します。

cdk init app --language typescript # cdk アプリのひな形を作成する。
npm i -S @aws-solutions-constructs/aws-cloudfront-s3 # aws-cloudfront-s3 ソリューションコンストラクトのインストール

jest はcdk init app により、インストールされます。

ディレクトリ構成

以降では、以下にテストコードファイルを作成して、テストコードを記述していきます。

.
└── test
    └── cloudfront-s3-construct.test.ts // コンストラクトに対するテストコード

アサーションテストの実行

まずは、テストコードを流してみます。

テストコードの記述

本記事では、test/cloudfront-s3-construct.test.ts の中身を次のように実装します。

import { Template, Match, Capture } from "aws-cdk-lib/assertions";
import { CloudFrontToS3 } from "@aws-solutions-constructs/aws-cloudfront-s3/lib";

describe("CloudFrontToS3", () => {
  let template: Template;

  beforeAll(() => {
    // When
    const app = new cdk.App();
    const stack = new cdk.Stack(app, "Test");
    new CloudFrontToS3(stack, "CloudFrontToS3", {}); // テスト対象のコンストラクト
    template = Template.fromStack(stack);
  });
  // Then (ここ以降にテストを実装していく。)
  //  - 例: CloudFrontの数量をカウントする例
  test("CloudFront: 数量が1である。", () => {
    template.resourceCountIs("AWS::CloudFront::Distribution", 1);
  });
});

テストコードを書くためには、CloudFormation の知識が必要ですが、手が止まってしまったら生成AIに頼りましょう。
例えば、AWS CodeWhisperer (※AWSのAI コードジェネレータサービス) を使用することで、単純なテストケースであれば、高精度で作成できます。

テスト実行

以下のコマンドで、jest を実行します。

  • 全体のテストファイルを実行
npx jest

※実行するテストファイル範囲は、jest.config.jsにて、指定ができます。

  • 指定したテストファイルを実行
npx jest {ファイルパス}

実行結果は以下になります。

実行結果
テストが成功し、コンストラクトの中に CloudFront が 1 リソース存在しているのが分かります。

テストパターン集

Template クラスのメソッド毎によく使用するテストパターンをまとめます。
また、エラーの出力例も記載しており、メソッド毎どのようなエラーが出力されるのか参考にしてください。

No. テストパターン サンプル 使用する Template メソッド
1 特定のサービスのリソース数量をカウントする。 S3 バケットの数量を確認する。 resourceCountIs(type, count)
2 特定のサービスにおける、指定のプロパティを持つリソースをカウントする。 アクセスログを出力する S3 バケットの数量を確認する。 resourcePropertiesCountIs(type,props, count)
3 特定のサービスにおける、指定のプロパティを持つリソースが存在するか確認する。 CloudFront がアクセスログを出力することをを確認する。 hasResourceProperties(type, props)
4 特定のサービスの全てのリソースが指定のプロパティを持つか確認する。 全ての S3 Bucket がバージョニングを有効にしていることをを確認する。 allResourcesProperties(type, props)
5 特定の論理 ID のリソースが指定のプロパティを持つか確認する。(応用) CloudFront アクセスログ用のバケットのバージョニングが有効であることを確認する。 findResources(type, props?)

1. 特定のサービスのリソース数量をカウントする

テストコード

resourceCountIs を使用して、S3 バケットの数量を確認する。

test("S3 Bucket: 数量が4である。", () => {
  template.resourceCountIs("AWS::S3::Bucket", 4);
});

エラー出力例

数量を 4 から 3 にして実行してみます。
失敗の場合、resourceCountIs では、リソースの実際の数量が出力されます。

2. 特定のサービスにおける、指定のプロパティを持つリソースをカウントする

テストコード

resourcePropertiesCountIs を使用して、アクセスログを出力する S3 バケットの数量を確認する。

test("ログ出力が有効になっているS3 Bucket: 数量が2である。", () => {
  template.resourcePropertiesCountIs(
    "AWS::S3::Bucket",
    {
      LoggingConfiguration: Match.anyValue(),
    },
    2,
  );
});

エラー出力例

数量を 2 から 3 にして実行してみます。
resourcePropertiesCountIs では、リソースの実際の数量が出力されます。

3. 特定のサービスにおける、指定のプロパティを持つリソースが存在するか確認する

テストコード

hasResourceProperties を使用して、CloudFront がアクセスログを出力することをを確認する。

test("いずれかのCloudfront: アクセスログを出力する", () => {
  template.hasResourceProperties("AWS::CloudFront::Distribution", {
    DistributionConfig: {
      Logging: {
        Bucket: Match.anyValue(),
      },
    },
  });
});

hasResourceProperties は条件に一致するリソースが1つでもあれば、Pass します。
その為、複数のリソースが条件に一致することが期待される場合には、allResourcesProperties を使用します。

エラー出力例

Logging Bucket が指定されていないことをテストするように修正してみます。(Match.anyValue() の部分をMatch.absent() に変更する。)
失敗の場合、hasResourceProperties では、実際のリソースのプロパティとエラー箇所が出力されます。

4. 特定のサービスの全てのリソースが指定のプロパティを持つか確認する

テストコード

全ての S3 Bucket がバージョニングを有効にしていることを確認する。

test("全ての S3 Bucket: バージョニングが有効である。", () => {
  template.allResourcesProperties("AWS::S3::Bucket", {
    VersioningConfiguration: {
      Status: "Enabled",
    },
  });
});

エラー出力例

全ての S3 Bucket: バージョニングが有効でないことをテストするように修正してみます。(Enabled の部分をDisabled に変更する。)
失敗の場合、allResourcesProperties では、条件に一致しないリソース論理 ID の一覧が出力されます。

5. 特定の論理 ID のリソースが指定のプロパティを持つか確認する(応用)

特定のリソースのプロパティチェックをする場合、運用でタグや物理名の命名規則があれば、それを条件に指定することもできますが、ない場合は論理 ID を使用します。

テストコード

CloudFront アクセスログ用のバケットのバージョニングが有効であることを確認する。

test("CloudFrontアクセスログ用S3 Bucket: バージョニングが有効である。", () => {
  // 条件に合致するリソースのオブジェクトを取得
  const resources = template.findResources("AWS::S3::Bucket", {
    Properties: {
      VersioningConfiguration: {
        Status: "Enabled",
      },
    },
  });
  // resourcesのキーが論理IDなので、resourcesに指定の論理IDが含まれるかテストする。
  expect(resources).toHaveProperty("CloudFrontToS3CloudfrontLoggingBucket8350BE9B");
});

findResources で取得した Object に論理 ID が含まれていることをテストしています。

エラー出力例

CloudFront アクセスログ用 S3 Bucket: バージョニングが有効でないことをテストするように修正してみます。(論理 IDCloudFrontToS3CloudfrontLoggingBucket8350BE9Bの部分をdummyに変更する。)
失敗の場合、findResources の結果オブジェクトが出力されます。

リソース間の参照をテストする方法

リソース間の参照をテストする必要がある場合があります。
例えば、Cloudfront のログ出力設定に特定の S3 バケットが設定されているかをテストしたいケースです。
この例では、Cloudformation テンプレートは以下になります。

CloudFormation テンプレート

CloudFrontToS3CloudFrontDistribution241D9866 :: {
  "Type": "AWS::CloudFront::Distribution",
  "Properties": {
      "DistributionConfig": {
          "Logging": {
              "Bucket": {
                  "Fn::GetAtt": [ "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", "RegionalDomainName" ]
              }
          },
          ...
      }
  },
}

この時、S3Bucket 論理 ID "CloudFrontToS3CloudfrontLoggingBucket8350BE9B"はどのように取得すればよいでしょうか?
2 パターンの方法があります。

1. hasResourceProperties で失敗させ、エラー出力の論理 ID を見て取得する方法

AWS CDK Workshopで紹介されている方法です。

プロダクトコードで"CloudFront アクセスログバケット"にアクセスログを出力する CloudFront が存在している状態で、次のようなテストコードを書いて実行し、わざと失敗させます。

テストコード

test("いずれかのCloudFront: CloudFrontアクセスログ用S3 Bucketにアクセスログを出力する。", () => {
  template.hasResourceProperties("AWS::CloudFront::Distribution", {
    DistributionConfig: {
      Logging: {
        Bucket: "Dummy",
      },
    },
  });
});

"CloudFront アクセスログバケット" の論理 ID を"Dummy"に指定して失敗させます。

実行結果

上記の実行結果に論理 ID が出力されるので、これを見て修正します。
S3 Bucket の論理 ID「CloudFrontToS3CloudfrontLoggingBucket8350BE9B」が含まれているので、「CloudFront アクセスログバケット」だということを確認して、テストコードを修正します。

テストコード

test("いずれかのCloudFront: CloudFrontアクセスログ用S3 Bucketにアクセスログを出力する。", () => {
  template.hasResourceProperties("AWS::CloudFront::Distribution", {
    DistributionConfig: {
      Logging: {
        Bucket: {
          "Fn::GetAtt": ["CloudFrontToS3CloudfrontLoggingBucket8350BE9B", "RegionalDomainName"],
        },
      },
    },
  });
});

論理IDはコンストラクトIDやパスが変更されると、論理IDも変更されるため注意してください。
ただし、論理IDが変更されるとリソースもリプレースになり、一般的に、検知したい内容だと思いますので、テストが失敗するのは問題ないはずです。

2. findResources で論理 ID を指定して検索する方法。

最初の方法で、テストの失敗結果から取得する方法にしっくり来ない方もいるかと思います。
その場合k、次のような方法も考えられます。
※ この方法の場合、「CloudFront アクセスログバケット」を一意に特定するキー情報(タグや物理名等)が必要です。

テストコード

test("いずれかのCloudFront: CloudFrontアクセスログ用S3 Bucketにアクセスログを出力する。", () => {
  // Nameタグ等をキーにfindResourcesで論理IDのリストを取得。
  const bucketLogicalIds = Object.keys(
    template.findResources("AWS::S3::Bucket", {
      Properties: {
        Tags: Match.arrayWith([
          {
            Key: "Name",
            Value: "CloudFrontAccessLogBucket",
          },
        ]),
      },
    }),
  );
  // 論理IDが一意であることを確認
  expect(bucketLogicalIds).toHaveLength(1);

  // CloudFrontのプロパティにS3 バケットの論理IDを指定する。
  template.hasResourceProperties("AWS::CloudFront::Distribution", {
    DistributionConfig: {
      Logging: {
        Bucket: {
          "Fn::GetAtt": [bucketLogicalIds[0], "RegionalDomainName"],
        },
      },
    },
  });
});

まとめ

今回は、CDK アサーションテストの基本について解説しました。

CTCは、AWSのビジネス利活用に向けて、お客様のステージに合わせた幅広い構築・運用支援サービスを提供しています。
経験豊富なエンジニアが、ワンストップかつ柔軟にご支援します。
ぜひ、お気軽にお問い合わせください。

お問い合わせ



【著者プロフィール】

山本 和輝(やまもと かずき)

伊藤忠テクノソリューションズ株式会社 クラウドアーキテクト

クラウドネイティブ技術を活用したシステム基盤の提案・構築や内製化支援等の案件を担当。

TOP>コラム一覧>AWS CDK 入門 - アサーションテストの基本

pagetop