aws-cdk-utul - AWS CDK単体テスト実装を高速化するライブラリをリリースしました

この記事は、[NTTテクノクロス Advent Calendar 2024 シリーズ1の1日目](https://qiita.com/advent-calendar/2024/ntt-tx)の記事になります。

皆さんこんにちは。NTTテクノクロスの堀江です。普段はAWSやAzure上でのシステム設計、構築や実装、調査検証系の案件を幅広く担当しています。

## はじめに

AWS CDKの単体テスト([きめ細かなアサーションテスト](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/testing.html#testing_fine_grained))の実装を高速化・効率化するためのライブラリ、[**aws-cdk-utul - AWS CDK Unit Test Utility Library**](https://github.com/horietakehiro/aws-cdk-utul)をこの度リリースしました。
なにやら抽象的な名前のライブラリですが、簡潔に述べると、**このライブラリを使用することで、IDEの型ヒントのサポートをフルに受けながらAWS CDKの単体テストを実装できるようになります。**

本記事では、[ライブラリの開発背景](#開発の背景)と[実際の使い方](#機能)をお話していきます。

---

## 開発の背景

AWS CDKのコンストラクトやスタックに対してきめ細かなアサーションテスト(以降、これを単体テストとして扱います)を行うための標準的なモジュールとして、[aws-cdk-lib/assertions](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.assertions-readme.html)モジュールが存在します。このモジュールを使用することで、自分たちが実装したコンストラクトやスタックから期待通りのCloudFormationテンプレートが合成(synthesize)されるかをテストできます。

```js:assertionsモジュールによる単体テストの実装例
import { aws_ec2 as ec2, Stack } from "aws-cdk-lib";
import { Match, Template } from "aws-cdk-lib/assertions";

const stack = new Stack();
new ec2.Vpc(stack, "VPC", {
  ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
  subnetConfiguration: [
    { subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, name: "public" },
  ],
  maxAzs: 2,
});
const template = Template.fromStack(stack);

test("サブネットマスクが/24のサブネットが2つ作成されることを確認する", () => {
  template.resourcePropertiesCountIs(
    "AWS::EC2::Subnet",
    {
      CidrBlock: Match.stringLikeRegexp("/24$"),
    },
    2
  );
});
```

しかし、(実際に使用経験がある方なら共感いただけると思いますが、)このモジュール単体では、単体テストを効率的に実装していくのは結構大変です。その大きな原因の一つが、テスト対象となる各種AWS CloudFormationリソースタイプ(上記の例であれば`AWS::EC2::Subnet`)に関する型情報が乏しく、テストコード実装時にIDEの型ヒント機能や言語そのものの型チェック機能の恩恵を殆ど得られないためです。

![型情報が乏しことの一例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/866958/73a94a3a-37a7-e09c-f093-9a9d94d784fb.png)

例えばTypeScriptのassertionsの場合、引数や戻り値として扱える各種リソースタイプの型情報は上記の通り、非常に型情報に乏しいです。

```js:型情報が乏しいことの弊害
import { aws_ec2 as ec2, Stack } from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";

const stack = new Stack();
new ec2.Vpc(stack, "VPC", {
  ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
  subnetConfiguration: [
    { subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, name: "public" },
  ],
  maxAzs: 2,
});
const template = Template.fromStack(stack);

test("サブネットマスクが/24のサブネットが2つ作成されることを確認する", () => {
  template.resourcePropertiesCountIs(
    "AWS::EC2::Subent", // リソースタイプも文字列でハードコードが必要なので、偶にスペルミスする
    {
      // CidrBlock: Match.stringLikeRegexp("/24$"),
      // プロパティ名や値の型を間違えてもコンパイルは通ってしまう。
      Cidrblock: 24,
    },
    2
  );
});
```

上記のように、単体テストコードの実装をうっかり間違えて単体テストが失敗した場合、テストコードの実装が間違っているのか、それとも本当にコンストラクトやスタックの実装が間違っているのか、調査するのに一手間要します。


**そこで開発したのが、以下のような機能を有するaws-cdk-utulです。**

---

## 機能

### TypedTemplate

**`TypedTemplate`クラスは、aws-cdk-lib/assertionsモジュールの`Template`クラスの使い勝手を維持しながら、(ほぼ)全てのAWS CloudFormationリソースタイプに関する適切な型情報を提供するため、IDEの型ヒント機能をフル活用して高速且つ効率的にAWS CDK単体テストを実装できます。**
これによって、前述したようなテストコード実装時の凡ミスや、些細な内容で公式ドキュメントをググる必要も大幅に減らせます。

![aws-cdk-utulの使用例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/866958/0ec9f6af-a3ef-817f-427c-70343e05eccd.png)

- aws-cdk-lib/assertionsモジュールの`Template`クラスの全てのメソッドを適切な型定義有りの状態で利用できます。
  ![TypedTemplateの実装メソッド](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/866958/2487a406-9fea-4fdb-6c61-0cad54c8b849.png)
- メソッド(例えば`findResoruces`)からの戻り値は、後続の処理でより利用し易いような構造に変更しています
  ```js:戻り値の使用例
    import { TypedTemplate } from "@horietakehiro/aws-cdk-utul/lib/assertions";
    import { AWS_EC2_SUBNET } from "@horietakehiro/aws-cdk-utul/lib/types/cfn-resource-types";
    import { aws_ec2 as ec2, Stack } from "aws-cdk-lib";
    
    const stack = new Stack();
    new ec2.Vpc(stack, "VPC", {
      ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
      subnetConfiguration: [
        { subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, name: "public" },
      ],
      maxAzs: 2,
    });
    const template = TypedTemplate.fromStack(stack);
    
    test("サブネットマスクが/24のサブネットが2つ作成されることを確認する", () => {
      // 従来の戻り値は{"リソースの論理ID" : {リソースの定義}}だが、
      // TypedTemplateの戻り値は、{id: リソースの論理ID, def: {リソースの定義}}のリストとしている
      const subnets = template.findResources(AWS_EC2_SUBNET());
      expect(subnets.length).toBe(2);
      subnets.forEach(({ id, def }) => {
        expect(id.includes("public")).toBe(true);
        expect(def.Properties?.CidrBlock?.endsWith("/24")).toBe(true);
      });
    });
  ```

- aws-cdk-lib/assertionsモジュールの`Matcher`クラスや任意のオブジェクト型も引き続き併用できます。
  ![Matchクラスも引き続き使用可能](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/866958/2398edd2-8b93-69b4-e418-570cd9ca2cef.png)

---

### ExtraMatch

※こちらはほぼおまけのような機能を提供するモジュールになります。

`ExtraMatch`クラスはAWS CDKの`Match`クラスに対する幾つかのシンタックスシュガーを提供します。

```js:ExtraMatchの使用イメージ
import {
    ExtraMatch,
    TypedTemplate,
} from "@horietakehiro/aws-cdk-utul/lib/assertions";
import {
    AWS_EC2_SUBNET,
    AWS_EC2_VPC,
} from "@horietakehiro/aws-cdk-utul/lib/types/cfn-resource-types";
import { CfnOutput, aws_ec2 as ec2, Stack } from "aws-cdk-lib";

const stack = new Stack();
const vpc = new ec2.Vpc(stack, "VPC", {
  ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
  subnetConfiguration: [
    { subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, name: "public" },
  ],
  maxAzs: 2,
});
new CfnOutput(stack, "VPCCidrBlock", {value: vpc.vpcCidrBlock})
const template = TypedTemplate.fromStack(stack);

test("サブネットマスクが/24のサブネットが2つ作成されることを確認する", () => {
  const [{ id }] = template.findResources(AWS_EC2_VPC());
  template.hasResource(
    AWS_EC2_SUBNET({
      Properties: {
        // VpcId: {Ref: id}と同義
        VpcId: ExtraMatch.ref(id),
      },
    })
  );
  // Value: {Fn::GetAtt: [id, "CidrBlock"]}と同義
  template.hasOutput("VPCCidrBlock", { Value: ExtraMatch.getAtt(id, "CidrBlock") });
});
```

---

## インストール方法

```bash:npmでインストール
npm install @horietakehiro/aws-cdk-utul
```

---

## 補足

- このライブラリで参照しているAWS CloudFormationリソースタイプのスキーマ情報は、[us-east-1(バージニアリージョン)](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/resource-type-schemas.html)のものを使用しています
- AWS CDK v2以上と互換性があります

---

## おわりに

aws-cdk-utulを使用することで、高速且つ効率的にAWS CDKの単体テストを実装できます。また今後も、単体テスト実装効率化の為の便利機能を(思いつき次第)本ライブラリに色々追加していきたいと思っています。

興味のある方は是非ご利用ください。

**明日のNTTテクノクロス Advent Calendar 2024は、 @subutakahiro  さんによる、GitHub Copilot for Xcodeについての記事です。お楽しみに！**