AWS CDKのテストを書いてみた

AWS CDKを利用してインフラ環境を作成したのでテストまで記述してみました。

CDKのテストは公式からモジュールが提供されているのでこれを利用してテストを書きます。
AWS CDKのテストには2つのカテゴリがあります。
- **アサーションテスト**　生成されたCloudFormationテンプレートに対して、リソースが特定のプロパティを持つかをテストします。改修やリファクタ時の回帰テストやテスト駆動開発にて新機能を開発する場合に役立ちます。

- **スナップショットテスト**　CDKスタックから合成したCloudFormationテンプレートに対して、以前保存したテンプレートとの差分を比較します。リファクタされたコードが元のコードと全く同じように動作することを確認できます。

使用しているリソースは主にECR, AppRunner, DynamoDBとなります。AppRunnerでフロントとバックエンドの環境を動かしています。

## アサーションテスト
スタックのインスタンスを作成してCloudFormationのテンプレートを生成するところから始まります。
`console.log(template.findResources('AWS::ECR::Repository'))` でマッチするリソースのセットを取得できます。

### ECRのテスト
特定のリポジトリ名のプロパティをもつECRの確認
```ts
  test('ECR repository stack', () => {
    const app = new cdk.App();
    const stack = new Cdk.ECRStack(app, 'ECRStack', { appName: 'app-prod' });
    const template = Template.fromStack(stack);
    
    template.hasResourceProperties('AWS::ECR::Repository',{
      RepositoryName: "app-prod-front",
    });
    template.hasResourceProperties('AWS::ECR::Repository',{
      RepositoryName: "app-prod-backend",
    });
  })
```
### DynamoDBのテスト
hasResourceはリソース定義全体をアサートしたい時に使えます。
```ts
  test('Infra stack', () => {
    const app = new cdk.App();
    const stack = new InfraStack(app, 'ApplicationInfra');
    const template = Template.fromStack(stack);

    template.resourceCountIs('AWS::DynamoDB::Table', 1);
    template.hasResource('AWS::DynamoDB::Table', {
      Properties: {
        TableName: 'Book',
        BillingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      },
      DeletionPolicy: "Delete",
    })
  })
```
### AppRunnerのテスト
AppRunnerはプロパティを持っているかのテストと環境変数がそれぞれセットされているかを確認するテストを記述しました。
`RuntimeEnvironmentSecrets`をcaputureして環境変数のkeyが存在するかのテストを行うようにしてみました。

```ts
  test('AppRunner stack', () => {
    const app = new cdk.App();
    const stack = new AppRunnerStack(app, 'ApplicationStack', appEnvProp);
    const template = Template.fromStack(stack);

    template.resourceCountIs('AWS::AppRunner::Service', 2);
    template.hasResourceProperties('AWS::AppRunner::Service', {
        ServiceName: `${appName}-front`,
        SourceConfiguration: {
          AutoDeploymentsEnabled: true,
          ImageRepository: {
            ImageRepositoryType: 'ECR',
            ImageConfiguration: {
              Port: "3000",
              RuntimeEnvironmentSecrets: Match.anyValue(),
            }
          },
        },
    });

    template.hasResourceProperties('AWS::AppRunner::Service', {
        ServiceName: `${appName}-backend`,
        SourceConfiguration: {
          AutoDeploymentsEnabled: true,
          ImageRepository: {
            ImageRepositoryType: 'ECR',
            ImageConfiguration: {
              Port: "8080",
              RuntimeEnvironmentSecrets: Match.anyValue(),
            }
          },
        },
    });
  })
  test('Is Env key setting in FrontEnd AppRunnerStack', () => {
    const app = new cdk.App();
    const stack = new AppRunnerStack(app, 'ApplicationStack', appEnvProp);
    const template = Template.fromStack(stack);
    const frontAppRunnerCapture = new Capture()
    template.hasResourceProperties('AWS::AppRunner::Service', {
        ServiceName: `${appName}-front`,
        SourceConfiguration: {
          ImageRepository: {
            ImageConfiguration: {
              RuntimeEnvironmentSecrets: frontAppRunnerCapture,
            }
          },
        },
    });
    expect(frontAppRunnerCapture._captured[0][0]).toHaveProperty("Name", "HOSTNAME")
  })
  test('Is Env key setting in Backend AppRunnerStack', () => {
    const app = new cdk.App();
    const stack = new AppRunnerStack(app, 'ApplicationStack', appEnvProp);
    const template = Template.fromStack(stack);
    const backendAppRunnerCapture = new Capture()

    template.hasResourceProperties('AWS::AppRunner::Service', {
      ServiceName: `${appName}-backend`,
      SourceConfiguration: {
        ImageRepository: {
          ImageConfiguration: {
            RuntimeEnvironmentSecrets: backendAppRunnerCapture,
          }
        },
      },
    });
    const backEnvironmentSecrets = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "DYNAMODB_KEY", "DYNAMODB_SECRET", "DYNAMODB_CONNECTION"];
    backendAppRunnerCapture._captured[0].forEach((value: any) => {
      expect(backEnvironmentSecrets.includes(value["Name"])).toBeTruthy()
    })
  })
```

## スナップショットテスト
各スタックのスナップショットの確認を行います
```ts
describe("snapshot test", () => {
  test("matches the ecr snapshot", () => {
    const app = new cdk.App();
    const stack = new ECRStack(app, "EcrStack");

    const template = Template.fromStack(stack);
    expect(template.toJSON()).toMatchSnapshot();
  });
  test("matches the infra snapshot", () => {
    const app = new cdk.App();
    const stack = new InfraStack(app, "ApplicationInfra");

    const template = Template.fromStack(stack);
    expect(template.toJSON()).toMatchSnapshot();
  });
  test("matches the AppRunner snapshot", () => {
    const app = new cdk.App();
    const stack = new AppRunnerStack(app, "ApplicationStack", {
      appName,
      envName,
      frontendRepositoryName,
      backendRepositoryName
    });

    const template = Template.fromStack(stack);
    expect(template.toJSON()).toMatchSnapshot();
  });
});
```

初回実行時に`_snapshots_`ディレクトリにスナップショットが作成されました。
![スクリーンショット 2024-06-11 14.01.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295926/278e7e8f-4d36-ae4f-14ae-2c31b66e0df2.png)

コードに変更が加わると前回のスナップショットと異なるのでテストはfailedになってしまいます。
`npm test -- -u`をすることでスナップショットを更新できます。

テストを実行するとアサーションテストとスナップショットテストそれぞれの結果を確認することができます。
![スクリーンショット 2024-06-11 14.01.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295926/75651114-f782-ea5d-58c7-07d4f0060c13.png)
### 参考
[Testing constructs](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/testing.html)

### 感想
意外と簡単にできた気がします。プロパティの確認などどこまで書くべきなのかは疑問
[AWS CDKのテストはどこまで書くべきか？](https://zenn.dev/cariot_dev/articles/4296d2e354507e)