AWS CDKで作った環境をinteg-testとinteg-runnerをつかって連結テストしてみる

# この記事について
2023年9月に開催された[CDK Day](https://www.cdkday.com/)でinteg-testとinteg-runnerの紹介がされておりました。  
CDKにおける連結テストの位置づけから、サンプルコードやテストの動かし方まで紹介されており、楽しそうだったので試してみました。その内容をご紹介いたします。

# この記事の対象者
- AWS CDKに興味がある方
- IaCでも自動テストに取り組んでみたい方
- integ-testとかinteg-runnerに興味がある方

# サンプルを動作させた環境

| # | ライブラリ | バージョン |
|:-----------:|:------------|:------------|
| 1     | node      | v18.17.1 |
| 2     | @aws-cdk/integ-runner      | 2.113.0-alpha.0 |
| 3     | @aws-cdk/integ-tests-alpha      | 2.113.0-alpha.0 |
| 4     | aws-cdk     | 2.113.0 |
| 5     | esbuild  | 0.19.8 |
| 6     | @types/aws-lambda  | 8.10.130 |

# integ-testとinteg-runnerについて
[CDK Day](https://www.cdkday.com/)のセッションでは、以下のように紹介されていました。

:::note warn
この機能は2023/12時点でα版の機能です。  
予告なく利用方法が変更される可能性がありますので、ご了承の上ご確認ください。
:::

## integ-testとは？
- `IntegTest`コンストラクタを提供し、テスト対象のCDKスタックをテストケースとして`IntegTest`に登録する
- `IntegTest`を通じて作成したインフラストラクチャを検査する

上記についてはこの後に出てくるサンプルコードを見ながら改めて解説いたします。

## integ-runnerとは？
- integ-testで実装したテストケースを実行するためのCLIで、テストケーススタックをdeployしたりdestroyしたりするもの
- CLIにはさまざまなオプションがあり、テスト実行時のふるまいを変更することができる。
- 既存のスナップショットと比較検査したり、テストを実行したあとにスナップショットを入れ替えたりする。

これも、サンプルコードを動かす際に実際のふるまいをこの後で紹介したいと思います。

# integ-test/integ-runnerで実施するテストについて
integration testの位置づけについても[CDK Day](https://www.cdkday.com/)で以下のように説明されていました。

- 2つ以上のコンポーネントを接続してテストをする
- 接続したコンポーネントが設計どおりにふるまうか検査する
- 潜在的な設定ミスをテストにより検出する  
IAMポリシーの設定ミス、サービスリミットの見過ごし、アプリケーション設定などが例に挙げられていました。

LambdaからDynamoDBにアクセスするような環境を作ったとして、それが動くかどうかを、実際にLambdaから動かして確認するテストを行うものだ、と筆者は理解しました。  
実際にどんなテストが行われるのか？はこの後のサンプルを見ながらみなさまも感じ取ってみてください。

# 事前準備
すでにCDKが実行できるAWS環境がある前提で説明いたします。  
手元でもサンプルを動かしてみたいかたは、環境のご準備をお願いします。  
ゼロから準備する方は[筆者が過去に投稿した記事](https://qiita.com/letsgomeow/items/e297d45103a182c86632#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99)をご確認ください。

サンプルを[GitHub](https://github.com/letsgomeow/integ-test-sample)に準備しました。  
`git clone https://github.com/letsgomeow/integ-test-sample.git`  
してご利用ください。

CloudShell環境など、AWSクレデンシャルが設定済みの環境で作業を行ってください。  
clone後のディレクトリで`npm install`を実施していただければ上に記載したライブラリ群が取得されます。これで準備完了です。

# 補足: CDKアプリケーションプロジェクトをゼロから作成する場合の注意点
integ-testとinteg-runnerはCDKv2に標準で含まれておりませんので、手動でインストールする必要があります。  
また、本サンプル内では`aws-cdk-lib/aws-lambda-nodejs`でLambda関数のビルドを行っていますので、esbuildのインストールも必要です。  
TypeScriptでLambdaを実装する際は、`@types/aws-lambda`もあったほうがよいですね。

```shell
npm install --save-dev @aws-cdk/integ-runner @aws-cdk/integ-tests-alpha
npm install --save-dev esbuild @types/aws-lambda
```

# テスト対象とテスト内容について
## テスト対象について
![202312_qiita01.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988239/79c7fa0d-92ba-e3c3-42ef-1ae917b8639a.png)

テスト対象のスタックとして、API Gatewayとバックエンドで動くLambdaを準備します。  
API Gatewayが公開するリソースにアクセスすると、LambdaがアクセスされたURLパスとhttpメソッドをJSONとして応答するシンプルなアプリケーションです。

[AWS公式ブログ](https://aws.amazon.com/jp/blogs/news/how-to-write-and-execute-integration-tests-for-aws-cdk-applications/)で紹介されていたサンプルはSNS->SQS->Lambda->DynamoDBの構成で、  
AWS SDKによるテストサンプルでしたので、httpによる検証サンプルをご提示しようと考え、この構成にしてみました。

## サンプルアプリケーションの構造
サンプルアプリケーションの構造は以下のとおりです。  
このあと、ポイントになる部分をピックアップしてご説明いたします。

```
├── bin
│   └── integ_test_sample.ts          # CDKエントリポイント
├── cdk.json
├── integ-tests
│   ├── helper.ts                     # integ-testヘルパークラス
│   └── integ.serverless.ts           # integ-testスタック
├── lib
│   ├── construct
│   │   ├── backend-lambda.ts         # <テスト対象> Lambdaコンストラクタ
│   │   ├── lambda
│   │   │   └── hello-world.ts        # <テスト対象> Lambda関数
│   │   └── rest-api.ts               # <テスト対象> API Gatewayコンストラクタ
│   └── stack
│       └── serverless-stack.ts       # <テスト対象> テスト対象のスタック
├── package.json
├── README.md
└── tsconfig.json
```

## テスト対象の実装
### API Gateway作成コンストラクタ
ポイントとなる部分をコード内のコメントで記載しているのでご確認ください。  

```typescript:rest-api.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

import * as iam from "aws-cdk-lib/aws-iam";
import * as apigateway from "aws-cdk-lib/aws-apigateway";

import { BackendLambdaConstruct } from "./backend-lambda";

/**
 * テスト対象のAPI Gatewayコンストラクタ
 */
export class RestApiConstruct extends Construct {
  // テスト対象のAPI Gateway。integ-runner側でテストすべきURLを設定するためにプロパティ公開する。
  public readonly restApi: apigateway.RestApi;
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // Lambda関数を作成する
    const backendFunction = new BackendLambdaConstruct(this, "BackendFunction");

    // API GatewayがLambdaを起動するために必要なIAMポリシーを作成する
    const apiGatewayLambdaExecutionPolicy = new iam.ManagedPolicy(
      this,
      "ApiGatewayLambdaExecutionPolicy",
      {
        managedPolicyName: "ApiGatewayLambdaExecutionPolicy",
        statements: [
          new iam.PolicyStatement({
            sid: "ApiGatewayLambdaExecution",
            effect: iam.Effect.ALLOW,
            actions: ["lambda:InvokeFunction"],
            resources: [
              `arn:aws:lambda:${cdk.Stack.of(this).region}:${
                cdk.Stack.of(this).account
              }:function:*`,
            ],
          }),
        ],
      },
    );
    // API GatewayがLambdaを起動するために必要なIAMロールを作成する
    const apiGatewayLambdaExecutionRole = new iam.Role(
      this,
      "ApiGatewayLambdaExecutionRole",
      {
        roleName: "ApiGatewayLambdaExecutionRole",
        assumedBy: new iam.ServicePrincipal("apigateway.amazonaws.com"),
        managedPolicies: [apiGatewayLambdaExecutionPolicy],
      },
    );

    // API Gatewayを作成する
    this.restApi = new apigateway.RestApi(this, "RestApi", {
      restApiName: "TestTargetApi",
      // CDKデプロイ時に、自動的にAPI Gatewayリソースもデプロイされるように設定
      deploy: true,
      deployOptions: {
        stageName: "test-stage",
      },
      retainDeployments: true,
      endpointConfiguration: {
        // サンプルとしてテストしやすいようにインターネット公開のAPI Gatewayを作成する
        types: [apigateway.EndpointType.EDGE],
      },
      cloudWatchRole: true,
      cloudWatchRoleRemovalPolicy: cdk.RemovalPolicy.DESTROY,
      // API Gatewayのリソース作成時にデフォルトで紐づけるLambda関数を登録する
      defaultIntegration: new apigateway.LambdaIntegration(
        backendFunction.function,
        {
          timeout: cdk.Duration.seconds(29),
          credentialsRole: apiGatewayLambdaExecutionRole,
        },
      ),
    });

    // API Gatewayにリソースを登録する。
    const foo = this.restApi.root.addResource("foo");
    foo.addMethod("GET");
    const bar = this.restApi.root.addResource("bar");
    bar.addMethod("POST");

    new cdk.CfnOutput(this, "URL", {
      value: `${this.restApi.url}/${this.restApi.deploymentStage.stageName}`,
    });
  }
}
```

### Lambda関数作成コンストラクタ
```typescript:backend-lambda.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

import * as lambda from "aws-cdk-lib/aws-lambda";
import * as lambdaNodeJS from "aws-cdk-lib/aws-lambda-nodejs";
import * as logs from "aws-cdk-lib/aws-logs";

import * as path from "path";

/**
 * Lambda関数作成コンストラクタ
 */
export class BackendLambdaConstruct extends Construct {
  public readonly function: lambda.IFunction;
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // integ-test側でremovalPolicyの上書き設定するためにここでロググループを作成しています。
    const logGroup = new logs.LogGroup(this, "BackendFunctionLog", {
      logGroupName: "/aws/lambda/BackendFunction",
      retention: logs.RetentionDays.ONE_DAY,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    // Lambda関数を登録する
    this.function = new lambdaNodeJS.NodejsFunction(this, "BackendFunction", {
      functionName: "BackendFunction",
      entry: path.join(__dirname, "lambda/hello-world.ts"),
      handler: "helloHandler",
      timeout: cdk.Duration.seconds(30),
      logGroup: logGroup,
      environment: {
        hoge: "fuga",
        bar: "baz",
        my: "value",
      },
    });
  }
}
```

### Lambda関数
```typescript:hello-world.ts
import * as lambda from "aws-lambda";

/**
 * Lambda関数
 *
 * @remarks
 * API Gatewayリソースされた際のパスと、httpメソッドをJSONオブジェクトとして応答する。</br>
 * 応答例: </br>
 * `{ "calledPath": "foo", "calledMethod": "GET"}`
 * @param event API Gatewayから転送されてくるパラメータ
 * @param context Lambdaコンテキスト
 */
export const helloHandler: lambda.APIGatewayProxyHandler = async (
  event,
  context,
) => {
  // API Gatewayから転送されてきたパラメータからアクセスパスとhttpメソッドを取得
  const calledPath = event.path;
  const calledMethod = event.httpMethod;

  // API Gatewayから転送されてきたパラメータやLambdaコンテキストをログ出力する。
  // 出力先は、Lambda関数登録時に紐づけたCloudWatch Logsのロググループ。
  const output = {
    log_type: "REST API response object",
    ...event,
  };
  console.log(JSON.stringify(output));
  console.log(JSON.stringify(context));

  // 以下のようなhttp応答を行う。httpボディにJSONオブジェクトを設定する。
  // `{ "calledPath": "foo", "calledMethod": "GET"}`
  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      calledPath: calledPath,
      calledMethod: calledMethod,
    }),
  };
};
```

### テスト対象の利用方法
CDKアプリケーションとして配備可能です。

```shell
npx cdk list   # -> ServerlessStack
npx cdk deploy # -> ServerlessStackがデプロイされる
```

deploy完了後、以下のコマンドを実行すると、API GatewayとLambdaの動きが確認できます。  
API GatewayのURLは、API Gatewayコンソールの「ステージ」画面で確認可能です。  
以下のcurlコマンドでAPI Gatewayにアクセスしてみてください。

```shell
curl https://<API ID>.execute-api.ap-northeast-1.amazonaws.com/test-stage/foo
    # -> `{"calledPath":"/foo","calledMethod":"GET"}`
```

確認ができたら、以下のコマンドを実行して、CDKアプリケーションを削除しておきましょう。
```shell
npx cdk destroy
```

:::note warn
上記手順でサンプルアプリケーションを削除したのち、CloudWatch Logsのロググループを管理画面から手動で削除しておいてください。`/aws/lambda/BackendFunction`というロググループが該当します。これを削除しておかないと、後のinteg-runner実行時にエラーになってしまいます。(理由は後述)
:::

## integ-test実装
integ-testでテストを実装してみました。  
テストの内容については、コード内のコメントをご参照ください。

```typescript:integ.serverless.ts
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import * as it from "@aws-cdk/integ-tests-alpha";

import { ServerlessStack } from "../lib/stack/serverless-stack";
import { ApplyDestroyPolicyAspect } from "./helper";

// テスト対象のCDKアプリケーションを作成
const app = new cdk.App();
// テスト対象のスタックを作成。このスタック内部でAPI GatewayとLambda関数が作成される。
const target = new ServerlessStack(app, "TestTargetStack");
// Lambda関数に紐づくCloudWatch LogsロググループのDeletionPolicyを上書きするアスペクトを登録。
cdk.Aspects.of(target).add(new ApplyDestroyPolicyAspect());

// integ-testコンストラクタ作成
// testCasesの配列にテスト対象のコンストラクタを登録する
const integ = new it.IntegTest(app, "IntegTest", {
  testCases: [target],
});

// テスト対象のスタックに対してアサーションを実施
const assersion = integ.assertions
  // API Gatewayリソースにhttp GETでアクセスする。
  // 本関数を登録することにより内部でLambda関数が登録され、httpアクセスを行う。
  .httpApiCall(`${target.restApiConstruct.restApi.url}foo`, {
    method: "GET",
    port: 443,
  })
  // 上記Lambda関数の応答内容をアサーションする。
  // httpのボディ部に以下のJSONが含まれるはず、というテストケースです。
  .expect(
    it.ExpectedResult.objectLike({
      body: { calledPath: "/foo", calledMethod: "GET" },
    }),
  );
```

## テストの動かし方(integ-runnerについて)
## テスト初回起動時
以下のコマンドを実行してみてください。

```shell
npx integ-runner --directory ./integ-tests --parallel-regions ap-northeast-1 --clean --verbose
```

なお、コマンドラインのオプションは以下の意味があります。

| # | オプション | 意味 |
|:-----------:|:------------|:------------|
| 1     | --directory      | integ-testクラスが存在するディレクトリを指定します。 |
| 2     | --parallel-regions      | テスト対象およびテストスタックを起動するリージョンを指定します。 |
| 3     | --clean      | テスト実行後、テスト対象およびテストスタックを自動で削除します。 |
| 4     | --verbose      | ログ出力レベルを変更します。(詳しいログ出力にします) |

ほかにも多数のオプションがありますので、詳細は[integ-runnerのGitHub](https://github.com/aws/aws-cdk/tree/main/packages/%40aws-cdk/integ-runner#options)をご確認ください。


以下の内容が表示され、テストが失敗します。

```
admin:~/environment/IntegTestSample (feature/app) $ npx integ-runner --directory ./integ-tests --parallel-regions ap-northeast-1 --clean --verbose

Verifying integration test snapshots...

  NEW        integ.serverless 8.229s

Snapshot Results: 

Tests:    1 failed, 1 total
Failed: /home/ec2-user/environment/IntegTestSample/integ-tests/integ.serverless.ts
Error: Some tests failed!
To re-run failed tests run: integ-runner --update-on-failed
    at main (/home/ec2-user/environment/IntegTestSample/node_modules/@aws-cdk/integ-runner/lib/index.js:10423:13)
admin:~/environment/IntegTestSample (feature/app) $ 
```

integ-runnerはテスト実行時に、前回のテスト結果スナップショットと比較を行います。  
差がある場合はエラー扱いされ、テストが停止します。

ちなみに、ここでいうスナップショットとは、前回テスト実行時に利用されたCloudFormation(CFn)テンプレートです。

テスト初回実行時は前回のスナップショットが存在しないため必ずエラーになります。  
警告文で指示されているとおり、`--update-on-failed`オプションを付与してもう一度実行してみましょう。

```shell
npx integ-runner --directory ./integ-tests --parallel-regions ap-northeast-1 --clean --verbose --update-on-failed
```

スナップショットとの比較はエラー判定されておりますが、テストはそのまま継続実行されます。  
CFn画面を眺めていてもらうと、テスト対象のスタックがまずデプロイされ、そのあとにinteg-testのスタックがデプロイされます。しばらく眺めていると、その後、自動的に両方のスタックが削除されます。

テストが完了すると、以下のように表示されます。

```
admin:~/environment/IntegTestSample (feature/app) $ npx integ-runner --directory ./integ-tests --parallel-regions ap-northeast-1 --clean --verbose --update-on-failed

Verifying integration test snapshots...

  NEW        integ.serverless 8.455s

Snapshot Results: 

Tests:    1 failed, 1 total
Failed: /home/ec2-user/environment/IntegTestSample/integ-tests/integ.serverless.ts

Running integration tests for failed tests...

Running in parallel across regions: ap-northeast-1
Running test /home/ec2-user/environment/IntegTestSample/integ-tests/integ.serverless.ts in ap-northeast-1
  SUCCESS    integ.serverless-IntegTest/DefaultTest 210.424s
       AssertionResultsHttpApiCall5b1e76c93573f6bf66c49f5c4caf5770 - success

Test Results: 

Tests:    1 passed, 1 total
   --- Integration test metrics ---
Profile undefined + Region ap-northeast-1 total time: 210.443
  /home/ec2-user/environment/IntegTestSample/integ-tests/integ.serverless.ts: 210.443
admin:~/environment/IntegTestSample (feature/app) $ 
```

上記のとおり、スナップショットの後に動いたテストはパスしています。

## テストアサーションの確認
本当にアサーションできているか心配になったので、わざと期待値を書き換えてテストしてみます。

```shell
npx integ-runner --directory ./integ-tests --parallel-regions ap-northeast-1 --clean --verbose --update-on-failed --force
```

`--force`オプションをつけております。テスト対象のコードに変更がない場合、  
スナップショットテストで「差分なし」と判定され、テストがパスしてしまいます。  
このオプションをつけることにより、スナップショットテストをパスしても後続の  
デプロイを行うテストが起動されます。

```
Running test /home/ec2-user/environment/IntegTestSample/integ-tests/integ.serverless.ts in ap-northeast-1
  ASSERT     integ.serverless-IntegTest/DefaultTest (undefined/ap-northeast-1) 215.401s
      AssertionResultsHttpApiCall5b1e76c93573f6bf66c49f5c4caf5770
{
  "body": {
    "calledMethod": "GET",
!!   Expected /baz but received /foo
    "calledPath": "/foo"
  },
  "headers": { ... },
  "ok": true,
  "status": 200,
  "statusText": "OK"
}

Test Results: 

Tests:    1 failed, 1 total
```

ちゃんとアサーションしてくれていましたね。`Expected /baz but received /foo`がまさに期待値を書き換えた箇所です。

## 2回目以降のinteg-runnerの動きについて
アサーション期待値実装を元に戻しましょう。

そして、テスト対象スタックを変更してみましょう。例として、以下の変更を加えてみます。  
rest-api.tsのAPIリソース作成箇所を以下のとおり修正します。
```diff_typescript
    const foo = this.restApi.root.addResource("foo");
    foo.addMethod("GET");
    const bar = this.restApi.root.addResource("bar");
    bar.addMethod("POST");
+    const baz = this.restApi.root.addResource("baz");
+    baz.addMethod("GET");
```

改めてテストを実施します。
```shell
npx integ-runner --directory ./integ-tests --parallel-regions ap-northeast-1 --clean --verbose --update-on-failed
```

スナップショットの差分として以下が表示されました。
```
Resources
[-] AWS::ApiGateway::Deployment ServerlessRestApiDeployment8B08A16E279c2e18d3476fe32566b3bee54a39a4 orphan
[+] AWS::ApiGateway::Deployment ServerlessRestApiDeployment8B08A16Ea5e648310a639408b32ac5bd97f21abc 
[+] AWS::ApiGateway::Resource ServerlessRestApibazDC2B6825 
[+] AWS::Lambda::Permission ServerlessRestApibazGETApiPermissionTestTargetStackServerlessRestApi944FD98EGETbaz2056DADE 
[+] AWS::Lambda::Permission ServerlessRestApibazGETApiPermissionTestTestTargetStackServerlessRestApi944FD98EGETbaz4AB3587B 
[+] AWS::ApiGateway::Method ServerlessRestApibazGETE6ADF6F7 
[~] AWS::ApiGateway::Stage ServerlessRestApiDeploymentStageteststage6CB8786A 
 └─ [~] DeploymentId
     └─ [~] .Ref:
         ├─ [-] ServerlessRestApiDeployment8B08A16E279c2e18d3476fe32566b3bee54a39a4
         └─ [+] ServerlessRestApiDeployment8B08A16Ea5e648310a639408b32ac5bd97f21abc
```

APIリソースを追加していますので、外部公開するためには新たに「Deployment」を作成し、ステージと紐づける必要があります。(自動で行われるようにCDK実装しております)  
もともとあったDeploymentが「orphan」として取り残されるぞ、と警告されています。  
これそのものは狙った変更です。

```
Failed: /home/ec2-user/environment/IntegTestSample/integ-tests/integ.serverless.ts
!!! This test contains destructive changes !!!
    Stack: TestTargetStack - Resource: ServerlessRestApiDeployment8B08A16E279c2e18d3476fe32566b3bee54a39a4 - Impact: WILL_DESTROY
!!! If these destructive changes are necessary, please indicate this on the PR !!!
```

その後、上記のような警告がでてテストそのものはエラー扱いになってしまいました。  
まだα版であるためか、「destructive changes」扱いされてしまっています。

## 2回目以降のinteg-runnerの動きについて(補足)
上記操作を行っている際、CFn画面を注意深く確認してもらいたいです。  
テスト対象のスタックが「create」され、その後「update」されることが確認できると思います。

この動きについてて、[integ-runnerのGitHub](https://github.com/aws/aws-cdk/tree/main/packages/%40aws-cdk/integ-runner#common-workflow)に説明がありました。

`If an existing snapshot is being updated, the integration test runner will first deploy the existing snapshot and then perform a stack update with the new changes. This is to test for cases where an update would cause a breaking change only on a stack update.`

まさに観測された動きと合致します。  
スタックの変更が破壊的かどうかを連結テスト時にも確認できるようにする、という意図が読み取れました。

[integ-runnerのGitHub](https://github.com/aws/aws-cdk/tree/main/packages/%40aws-cdk/integ-runner#usage)でも以下のように記載されており、スナップショットそのものもGitで管理するよう推奨されています。

`All snapshot files (i.e. *.snapshot/**) must be checked-in to version control. If not, changes cannot be compared across systems.`

## integ-testヘルパークラスについて(補足)
テスト対象のコンストラクト内でCloudWatch LogGroupを作成しております。  
本番運用を意識して、RetentionPoilcyをRETAINとして作成しております。  

integ-testでテストを行う際は本ログを削除しておきたいところです。  
(本サンプルでは、2回目以降のテスト実行時に名前重複してエラーになります。)

integ-test側からRetentionPolicyをDESTROYに上書きすることでこれを実現しています。

```typescript:helper.ts
import * as cdk from "aws-cdk-lib";
import { IConstruct } from "constructs";
import * as logs from "aws-cdk-lib/aws-logs";

/**
 * integ-testヘルパークラス
 *
 * @remarks
 * テスト対象スタックにこのAspectを付与することにより、該当リソースのRemovalPolicyをDESTROYに上書きする。
 */
export class ApplyDestroyPolicyAspect implements cdk.IAspect {
  public visit(node: IConstruct) {
    if (node instanceof logs.CfnLogGroup) {
      node.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
    }
  }
}
```

[CDKのAspect](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/aspects.html)という機能を利用しています。

```typescript:integ.serverless.ts
// テスト対象のCDKアプリケーションを作成
const app = new cdk.App();
// テスト対象のスタックを作成。このスタック内部でAPI GatewayとLambda関数が作成される。
const target = new ServerlessStack(app, "TestTargetStack");
// Lambda関数に紐づくCloudWatch LogsロググループのDeletionPolicyを上書きするアスペクトを登録。
cdk.Aspects.of(target).add(new ApplyDestroyPolicyAspect());
```

integ-test実装の中で、上記コードによりServerlessStackにAspectを適用しています。

ちなみに、[AWSブログ](https://aws.amazon.com/jp/blogs/news/how-to-write-and-execute-integration-tests-for-aws-cdk-applications/)でも上記は紹介されているのですが、テスト対象スタック側に上記実装が含まれておりました。  
本サンプルでは、この機能はテスト時のみに利用する機能であるととらえて、integ-test側のコードに配置しています。

# 利用してみて気づいた注意点
## 複数環境に配備する考慮が必要
本サンプルで以下の操作を行うと、テストが失敗します。  
コメントに記載したような場面をイメージしております。実運用でも遭遇する場面だと思います。

```shell
# 総合テスト環境相当のものをデプロイ
npx cdk deploy
# その後、CI的にinteg-testを実行
npx integ-runner --directory ./integ-tests --parallel-regions ap-northeast-1 --clean --verbose --update-on-failed
```

CDK内で作成するリソースの名前が重複するのが原因です。

```typescript
    // integ-test側でremovalPolicyの上書き設定するためにここでロググループを作成しています。
    const logGroup = new logs.LogGroup(this, "BackendFunctionLog", {
      logGroupName: "/aws/lambda/BackendFunction",   # ここ！
      retention: logs.RetentionDays.ONE_DAY,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });
 
    // Lambda関数を登録する
    this.function = new lambdaNodeJS.NodejsFunction(this, "BackendFunction", {
      functionName: "BackendFunction",               # ここ！
      entry: path.join(__dirname, "lambda/hello-world.ts"),
      handler: "helloHandler",
      timeout: cdk.Duration.seconds(30),
      logGroup: logGroup,
      environment: {
        hoge: "fuga",
        bar: "baz",
        my: "value",
      },
    });
  }
```

logGroupNameやfunctionNameを指定していますが、固定の値を入れているため後から実行したinteg-test内で  
作成するスタックが名前重複することにより失敗します。

名前を明示せずCDKで名前を自動付与するか、環境により名前を切り替える考慮が必要です。

## テスト実施時の手触り
テスト対象のCFnスタックを配備して、変更して、テストを実行して削除してという流れで  
それなりに時間がかかります。integ-runnerを実行した画面に進捗を表す情報が出てこないので精神衛生上よろしくないのですが、ガマンして待っていてください。

心配な方は、CFn画面を見ていてください。スタックの状態が変わっていくのでテストの進行状況がなんとなくわかると思います。

通常のCDKと同様に、プログレスバーを出してくれるなど、integ-runnerに機能追加されるとよいですね。

## アサーション方法について
2023/12時点でマニュアルを確認すると、3種類の方法が公開されています。  
`@aws-cdk/integ-tests-alpha >> IApiCall`の実装クラスである、以下の3クラスが該当します。

| # | クラス | 用途 |
|:-----------:|:------------|:------------|
| 1     | AwsApiCall      | AWS SDKを利用した検査 |
| 2     | HttpApiCall      | http/httpsアクセスによる検査。ただしインターネット公開アプリのみ。 |
| 3     | LambdaInvokeFunction      | Lambda関数による検査 |

LambdaInvokeFunctionはまだ試していないのですが、Lambda関数そのものを登録できるため、  
例えば、LambdaをVPC内で起動し、適切なセキュリティグループを付与することにより、VPC内のアプリケーションにも検査を行うことができるかもしれません。

# 所感
## integ-testによる連結テストについて
筆者が勝手に勘違いをしていたのですが、「連結テスト」という名前から、プロジェクトでテストや本番運用で継続利用する環境をCDKで作成し、  
そこに対するテストをinteg-testで行うものだと考えておりました。

上に示したサンプルを見ていただくと自明ですが、そうではありません。

環境がコードで表現されているわけですから、配備先の条件が同じであれば、同じ振る舞いをするはずだ、  
という考えに基づいたテストツールだと筆者は理解いたしました。

## テスト生産性について
実際にテストを実装してみて思ったことですが、正直、開発者体験としてはツラいものがあります。  
テストケースを追加してinteg-runnerを実行して検証・・・の流れがどうにも時間がかかるため生産性があまりよろしくないです。

今回のサンプルはきわめて小規模なものですが、コマンド実行から結果がわかるまで4～5分ほどかかります。  
これだけ待ってからテストがFailするとけっこうがっかりします。(わたしだけではないはず)

CDKで配備する構成をとらえて、いわゆるハッピーパスに絞ったテストを作成するのがよいと思います。

## 筆者が考える使いどころ
否定的なことを書いてはおりますが、実運用でも活用していけるのではないでしょうか。

- CDKバージョンアップを積極的に行うことができる
CDKは相変わらずものすごいスピードでバージョンが上がっていきます。  
一度作成したCDKアプリケーションも継続利用するのであればバージョン追随していきたいですね。  
integ-runnerを利用すればすぐにスナップショットテストが有効になりますので、  
ここの部分だけでも利用する価値はあるのではないでしょうか。  
CDKバージョンアップ前にテストを動かしてスナップショットを生成し、  
CDKバージョンアップ後にもう一度テストを動かして差がないことを確認するイメージです。

- 環境複製時の正常稼働保証
筆者はCDKで作成した環境を複数環境(連結テスト環境、総合テスト環境、性能テスト環境・・・)と  
増殖させていくことが多いのですが、このときに、integ-testが実行されていれば  
より信頼度が増すのではないかと考えます。(CIにより継続的にテストが実行されている前提ですが)  
また、integ-testでテストが記載されていることにより、環境複製時の正常確認も、  
手動ではありますが同じことを行えばよい、ということで信頼度が増すと思います。  
とわいえ、配備済みの環境に対してのテストも実行できるようにinteg-testが発展してくれるのを祈りたい。

# 参考
- integ-testマニュアル  
https://docs.aws.amazon.com/cdk/api/v2/docs/integ-tests-alpha-readme.html
- ingeg-runnerマニュアル  
https://github.com/aws/aws-cdk/tree/main/packages/%40aws-cdk/integ-runner
- AWSブログ  
https://aws.amazon.com/jp/blogs/news/how-to-write-and-execute-integration-tests-for-aws-cdk-applications/
- CDK Day  
https://www.cdkday.com/
- CDK マニュアル
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/getting_started.html