環境毎にパラメーターの異なるスタックの AWS CDK スナップショットテスト(Jest)を構成する

2024.03.13

こんにちは、CX 事業本部製造ビジネステクノロジー部の若槻です。

AWS CDK のコンストラクトのテストには、作成されるリソースのプロパティをテストする Fine-grained assertions test と、生成されたテンプレート全体をテストする Snapshot test(スナップショットテスト) があります。

今回は、後者のスナップショットテストについて、環境毎に異なるパラメーターを使用する場合のテスト方法を確認してみました。

環境

バージョン
jest 29.7
aws-cdk 2.127.0

やってみた

CDK 環境初回作成

cdk init コマンドにより CDK プロジェクト(TypeScript)を作成します。

mkdir cdk_demo_project
cd $_
cdk init --language typescript

単一環境の場合のテスト

まず、環境毎のパラメーターを使用しない単一環境の場合のテストを試してみます。

CDK スタックの定義です。

lib/cdk_demo_project-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as sqs from 'aws-cdk-lib/aws-sqs';

export class CdkDemoProjectStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new sqs.Queue(this, 'CdkDemoProjectQueue', {
      visibilityTimeout: cdk.Duration.seconds(300)
    });
  }
}

CDK アプリケーションの定義です。

bin/cdk_demo_project.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkDemoProjectStack } from '../lib/cdk_demo_project-stack';

const app = new cdk.App();
new CdkDemoProjectStack(app, 'CdkDemoProjectStack', {});

スナップショットテストの記述です。

test/cdk_demo_project.test.ts

import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import * as CdkDemoProject from '../lib/cdk_demo_project-stack';

test('Snapshot', () => {
   const app = new cdk.App();

   const stack = new CdkDemoProject.CdkDemoProjectStack(app, 'MyTestStack');
   const template = Template.fromStack(stack).toJSON();

   expect(template).toMatchSnapshot();
});

テストを実行するとパスしました。

$ npx jest test/cdk_demo_project.test.ts
 PASS  test/cdk_demo_project.test.ts
  ✓ Snapshot (128 ms)

 › 1 snapshot written.
Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 written, 1 total
Time:        2.373 s, estimated 3 s
Ran all test suites matching /test\/cdk_demo_project.test.ts/i.

初回実行後は __snapshots__ ディレクトリ配下にスナップショットファイルが作成されます。

test/__snapshots__/cdk_demo_project.test.ts.snap

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Snapshot 1`] = `
{
  "Parameters": {
    "BootstrapVersion": {
      "Default": "/cdk-bootstrap/hnb659fds/version",
      "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
      "Type": "AWS::SSM::Parameter::Value<String>",
    },
  },
  "Resources": {
    "CdkDemoProjectQueue4FD1BC37": {
      "DeletionPolicy": "Delete",
      "Properties": {
        "VisibilityTimeout": 300,
      },
      "Type": "AWS::SQS::Queue",
      "UpdateReplacePolicy": "Delete",
    },
  },
  "Rules": {
    "CheckBootstrapVersion": {
      "Assertions": [
        {
          "Assert": {
            "Fn::Not": [
              {
                "Fn::Contains": [
                  [
                    "1",
                    "2",
                    "3",
                    "4",
                    "5",
                  ],
                  {
                    "Ref": "BootstrapVersion",
                  },
                ],
              },
            ],
          },
          "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.",
        },
      ],
    },
  },
}
`;

環境毎のパラメーターが異なる場合のテスト

続いて、環境毎のパラメーターを使用する場合のテストを試してみます。

環境毎のパラメーターを定義する方法はいくつかあると思いますが、今回は次のように context.json で定義する場合を想定します。

cdk.json

{
  "context": {
    "domainName": {
      "dev": "dev.example.com",
      "prd": "example.com"
    }
  }
}

スタックの定義を、環境毎のパラメーターを受け取るように修正します。

lib/cdk_demo_project-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as sqs from 'aws-cdk-lib/aws-sqs';

interface CdkDemoProjectStackProps extends cdk.StackProps {
  readonly stageName: "dev" | "prd";
  readonly domainName: string;
}

export class CdkDemoProjectStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: CdkDemoProjectStackProps) {
    super(scope, id, props);

    new sqs.Queue(this, 'CdkDemoProjectQueue', {
      visibilityTimeout: cdk.Duration.seconds(300)
    });
  }
}

アプリケーションの定義も、環境毎のパラメーターを受け取るように修正します。コンテキストのステージ名に応じて context.json からパラメーターを取得します。

bin/cdk_demo_project.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkDemoProjectStack } from '../lib/cdk_demo_project-stack';

const app = new cdk.App();
const stageName: "dev"| "prd" = app.node.tryGetContext('stageName');
const domainName =
  app.node.tryGetContext('domainName')[stageName];

new CdkDemoProjectStack(app, 'CdkDemoProjectStack', {
  stageName,
  domainName
});

さて、それぞれの環境に対するスナップショットテストを記述します。

次のようにテストを実行する関数を作成します。各環境のステージ名を引数として、その環境のスタックを作成し、スナップショットテストを実行するという処理です。

test/stack-test.ts

import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import * as CdkDemoProject from '../lib/cdk_demo_project-stack';
import { context } from '../cdk.json';

export const snapshotStackTests = (stageName: "dev" | "prd") => {
  const app = new cdk.App();

  const domainName: string = context["domainName"][stageName];

  const stack = new CdkDemoProject.CdkDemoProjectStack(app, `${stageName}-MyTestStack`, {
    stageName,
    domainName
  });
  const template = Template.fromStack(stack).toJSON();
  
  test("Snapshot", () => {
    expect(template).toMatchSnapshot();
  });
}

そして先ほどの関数を環境毎の引数を使用して呼び出すテストファイルを作成します。

test/main.test.ts

import { snapshotStackTests } from './stack-test';

describe('Development Environment', () => {
  snapshotStackTests('dev');
});

describe('Production Environment', () => {
  snapshotStackTests('prd');
});

スナップショットを更新してみます。

npx jest test/main.test.ts -u

すると、環境毎のスナップショットファイルが作成されました。

test/__snapshots__/main.test.ts.snap

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Development Environment Snapshot 1`] = `
{
  "Parameters": {
    "BootstrapVersion": {
      "Default": "/cdk-bootstrap/hnb659fds/version",
      "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
      "Type": "AWS::SSM::Parameter::Value<String>",
    },
  },
  "Resources": {
    "CdkDemoProjectQueue4FD1BC37": {
      "DeletionPolicy": "Delete",
      "Properties": {
        "VisibilityTimeout": 300,
      },
      "Type": "AWS::SQS::Queue",
      "UpdateReplacePolicy": "Delete",
    },
  },
  "Rules": {
    "CheckBootstrapVersion": {
      "Assertions": [
        {
          "Assert": {
            "Fn::Not": [
              {
                "Fn::Contains": [
                  [
                    "1",
                    "2",
                    "3",
                    "4",
                    "5",
                  ],
                  {
                    "Ref": "BootstrapVersion",
                  },
                ],
              },
            ],
          },
          "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.",
        },
      ],
    },
  },
}
`;

exports[`Production Environment Snapshot 1`] = `
{
  "Parameters": {
    "BootstrapVersion": {
      "Default": "/cdk-bootstrap/hnb659fds/version",
      "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
      "Type": "AWS::SSM::Parameter::Value<String>",
    },
  },
  "Resources": {
    "CdkDemoProjectQueue4FD1BC37": {
      "DeletionPolicy": "Delete",
      "Properties": {
        "VisibilityTimeout": 300,
      },
      "Type": "AWS::SQS::Queue",
      "UpdateReplacePolicy": "Delete",
    },
  },
  "Rules": {
    "CheckBootstrapVersion": {
      "Assertions": [
        {
          "Assert": {
            "Fn::Not": [
              {
                "Fn::Contains": [
                  [
                    "1",
                    "2",
                    "3",
                    "4",
                    "5",
                  ],
                  {
                    "Ref": "BootstrapVersion",
                  },
                ],
              },
            ],
          },
          "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.",
        },
      ],
    },
  },
}
`;

注意点

次のように app の初期化をテスト用の関数のスコープ外に置くと、合成結果のテンプレートがキャッシュされて、2 つ目以降の環境のスナップショットテストでエラーが発生します。

test/stack-test.ts

import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import * as CdkDemoProject from '../lib/cdk_demo_project-stack';
import { context } from '../cdk.json';

const app = new cdk.App();

export const snapshotStackTests = (stageName: "dev" | "prd") => {
  const domainName: string = context["domainName"][stageName];

  const stack = new CdkDemoProject.CdkDemoProjectStack(app, `${stageName}-MyTestStack`, {
    stageName,
    domainName
  });
  const template = Template.fromStack(stack).toJSON();
  
  test("Snapshot", () => {
    expect(template).toMatchSnapshot();
  });
}

テストを実行すると次のように Unable to find artifact with id <スタック名> というエラーとなります。

$ npx jest test/main.test.ts -u
 FAIL  test/main.test.ts
  ● Test suite failed to run

    Unable to find artifact with id "prd-MyTestStack"

      14 |       domainName
      15 |     });
    > 16 |     const template = Template.fromStack(stack).toJSON();
         |                               ^
      17 |   
      18 |   test("Snapshot", () => {
      19 |     expect(template).toMatchSnapshot();

      at CloudAssembly.getStackArtifact (node_modules/aws-cdk-lib/cx-api/lib/cloud-assembly.js:1:2553)
      at toTemplate (node_modules/aws-cdk-lib/assertions/lib/template.js:2:468)
      at Function.fromStack (node_modules/aws-cdk-lib/assertions/lib/template.js:1:1587)
      at snapshotStackTests (test/stack-test.ts:16:31)
      at test/main.test.ts:8:21
      at Object.<anonymous> (test/main.test.ts:7:1)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        2.452 s, estimated 3 s
Ran all test suites matching /test\/main.test.ts/i.

app の初期化位置には注意しましょう。

参考

以上