CDK のテストに Vitest を使う

2024/12/07に公開

『AWS CDK Advent Calendar 2024』7 日目の記事です。

https://qiita.com/advent-calendar/2024/aws-cdk

はじめに

この記事では、CDK のテストに Vitest を使用する方法と、Vitest を使用したテストの書き方を紹介します。

Vitest とは?

Vitest とは Vite を利用した次世代の JavaScript テストフレームワークです

https://vitest.dev/

Vite というのは次世代フロントエンドツールですが、Vitest はフロントエンドに限らず、JavaScript / TypeScript で記述されたコードのテストを実行できます。

実際に私は CDK プロジェクトの Validation Test / Fine-grained assertions Test / Snapshot Test を全て Vitest で実行しています。
(各テストの詳細については以下の記事が参考になりました)

https://aws.amazon.com/jp/builders-flash/202411/learn-cdk-unit-test/

Vitest 導入手順

以下の手順で、CDK プロジェクトに Vitest を導入できます

1. インストールコマンドの実行

以下のコマンドを実行して、Vitest をインストールします

# npm
npm install --save-dev vitest

# yarn
yarn install -D vitest

# pnpm
pnpm install -D vitest

2. package.json の設定

続いて、ターミナル上で Vitest のコマンドが実行できるように、package.json の scriptsに test コマンドを追加します
(npx vitestでも実行可能なので、scriptsへの追加は必須ではありません)

package.json
{
  "scripts": {
+   "test": "vitest",
  }
}

3. Vitest の設定ファイル作成

続いて、プロジェクトのルートにvitest.config.tsを作成します
(ファイル拡張子はts以外にも指定できます)

https://vitest.dev/guide/#configuring-vitest

以下に、vitest.config.tsの例を紹介します

import { defineConfig } from "vitest/config";

export default defineConfig({
  root: ".",
  test: {
    root: ".",
    globals: true,
    environment: "node",
    include: ["**/test/**/*.{spec,test}.ts"]
  },
});

4. tsconfig.json の更新

Vitest をグローバルに使用したい場合(global API を宣言無く呼び出せるようにしたい場合)、vitest の型情報を読み込ませるためにtsconfig.jsoncompilerOptions.typesを修正する必要があります。

https://vitest.dev/config/#globals

修正内容は以下の通りです

tsconfig.json
{
  "compilerOptions": {
    // ...
+   "types": ["vitest/globals"]
  },
}


ここまでで、Vitest のセットアップは完了です。
以降では、Vitest を使用した Validation Test / Fine-grained assertions Test / Snapshot Test の記述方法を紹介します。

Validation Test

バリデーションとは、条件分岐などを通して値の妥当性を検証する処理のことです。AWS CDK においても、Stack や Construct への入力である props のプロパティに対してバリデーション処理を実装することがあります。
引用: https://aws.amazon.com/jp/builders-flash/202411/learn-cdk-unit-test/

今回は、RDS のデータベース名をバリデーションすることを想定します。

Construct 側のコードは以下の通りです

constructs/rds.ts
interface RdsProps {
  readonly databaseName: string;
  // ...
}

export class Rds extends Construct {
  constructor(scope: Construct, id: string, props: RdsProps) {
    super(scope, id);
    this.validateDatabaseName(props.databaseName);
    this.validateUsername(props.username);
    // ...
  }

  private validateDatabaseName(databaseName: string): void {
    if (!this.isSnakeCase(databaseName)) {
      throw new Error("データベース名はスネークケースで指定してください。");
    }
  }

  private isSnakeCase(str: string): boolean {
    const snakeCasePattern = /^[a-z]+(_[a-z]+)*$/;
    return snakeCasePattern.test(str);
  }
}

こちらのバリデーションに対するテストコードは以下の通りです

test/constructs/rds.test.ts
const createRdsInstance = (databaseName: string): Rds => {
  const app = new App();
  const stack = new Stack(app, "TestStack");
  // ...
  return new Rds(stack, "TestRds", {
    // ...
    databaseName,
  });
};

describe("バリデーションテスト", () => {
  test("データベース名がスネークケースでない場合はエラー", () => {
    const databaseName = "invalid-name";

    const errorMessage = "データベース名はスネークケースで指定してください。";

    expect(() => createRdsInstance(databaseName)).toThrowError(errorMessage);
  });
});

Fine-grained assertions Test

AWS CDK における Fine-grained assertions テストとは、生成された CloudFormation テンプレートの一部を取り出して、その部分に対してチェックを行うテストのことです。これにより、どのようなリソースが生成されるのかといった細かい構成要素に対するテストをすることができます。
引用: https://aws.amazon.com/jp/builders-flash/202411/learn-cdk-unit-test/

今回は、CloudFormation テンプレートの AWS::RDS::DBInstanceEngine, EngineVersion, DBInstanceClass を検証することを想定します

テストコードは以下の通りです

const getTemplate = (): Template => {
  const app = new App();
  const stack = new BackCdkTrainingStack(app, "TestStack");
  return Template.fromStack(stack);
};

describe("RDS Fine-grained assertions tests", () => {
  const template = getTemplate();
  test("RDSの設定が適切か", () => {
    template.hasResourceProperties("AWS::RDS::DBInstance", {
      Engine: "mysql",
      EngineVersion: "8.0",
      DBInstanceClass: "db.t4g.micro",
    });
  });
});

Snapshot Test

AWS CDK におけるスナップショットテストとは、CDK コードから合成される AWS CloudFormation テンプレートを出力し、以前のテスト実行時に生成したテンプレートの内容と比較してテンプレートの差分を検出するテストのことです。
引用: https://aws.amazon.com/jp/builders-flash/202411/learn-cdk-unit-test/

Snapshot Test を行う際に、CloudFormation テンプレートに付与されたハッシュ値をシリアライズする処理を行います。
これは、テストの実行ごとにハッシュ値が変わる場合(ECR の Image など)に対応するためです。

シリアライズを行う際には、Vitest の Custiom Serializer を使用します

https://vitest.dev/guide/snapshot#custom-serializer

plugins/vitestPlugin/customSerializer.ts
import { SnapshotSerializer } from "vitest";

export default {
  /**
   * シリアライズは、文字列のみを対象とする
   */
  test(val: unknown) {
    return typeof val === "string";
  },
  /**
   * ハッシュ値を置換する
   */
  serialize(val: string) {
    return val.replace(/[A-Fa-f0-9]{64}/, "hashed");
  },
} satisfies SnapshotSerializer;

そして、こちらの Serializer を config ファイルに追加します

vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  root: ".",
  test: {
    root: ".",
    globals: true,
    environment: "node",
+   snapshotSerializers: ["./plugins/vitestPlugin/customSerializer.ts"],
    include: ["**/test/**/*.{spec,test}.ts"]
  },
});

テストコードは以下の通りです。

test/sample-stack.test.ts

const getTemplate = (): Template => {
  const config = getConfigStackProps("dev");
  const app = new App();
  const stack = new BackCdkTrainingStack(app, "BackCdkTraining", config);
  return Template.fromStack(stack);
};

test("Snapshot Tests", () => {
  const template = getTemplate();
  const json = template.toJSON();

  expect(json).toMatchSnapshot();
});

Jest からの移行

cdk initコマンドを使用して JavaScript / TypeScript の CDK プロジェクトを開始すると、自動で Jest というテストフレームワークがインストールされます。

そのため、テストフレームワークに Jest を採用している CDK ユーザーも少なくないのではと考えています。

もし、すでに Jest を使っている CDK ユーザーの中で、Vitest への移行を検討している方がおりましたら、以下のドキュメントを参照ください
(Vitest は Jest と互換性のある API が提供されているため、簡単に移行することができます)

https://vitest.dev/guide/migration.html#migrating-from-jest

まとめ

今回は、CDK プロジェクトに Vitest を導入する方法を紹介しました。
Vitest の導入を検討されている方、Vitest を使用したテストの書き方に悩んだいる方の役に立てば幸いです!

参考資料

https://vitest.dev/

https://aws.amazon.com/jp/builders-flash/202411/learn-cdk-unit-test/

Discussion