aws cdkの自動テストをvitestで実行する
動機
cdk プロジェクト(TypeScript)は立ち上げたときに jest(ts-jest)を使う形で自動テストの雛形が自動セットアップされている。
基本それをベースに改修していく形で困らない。
が、ts-jest は実行が遅かったりする。型チェックをしてくれるというのはあるが、cdk におけるテストでは恩恵はあまり多くない気もする。
TypeScript を利用する cdk プロジェクトではビルドに esbuild が使われるため、速度が問題であればesbuild-jestを使う手もある。が、執筆時点における最終アップデートが 2021 年 5 月だったりとメンテナンスが頻繁でなく心許なかったりする。
そこで、vitestを使ってみる。
検証環境
- Fedora37
- node v18.15.0(LTS)
- pnpm@8.1.1
- あまり本質ではないが今回はパッケージマネージャーに pnpm を使う
- (npm や yarn を使う人は適当に読み替えてください)
- aws-cdk@2.73.0
実行
(1)初期セットアップ
cdk プロジェクトを立ち上げる適当なディレクトリ(今回はcdk-app
とする)を作成し、使用言語に TypeScript を指定して立ち上げる
# プロジェクトを立ち上げるディレクトリの作成
mkdir -p /path/to/cdk-app
cd /path/to/cdk-app
# プロジェクトの立ち上げ
npx aws-cdk@2.73.0 init --language typescript
rm package-lock.json # pnpmを使うため。npmを使う人はそのまま残せばOK
pnpm install # 使うpackageManagerに応じて適当に読み替え(以降も同様)
ここで、ディレクトリ構成は以下のような感じ
tree --gitignore
# .
# ├── README.md
# ├── bin
# │ └── cdk-app.ts
# ├── cdk.json
# ├── jest.config.js
# ├── lib
# │ └── cdk-app-stack.ts
# ├── package.json
# ├── pnpm-lock.yaml
# ├── test
# │ └── cdk-app.test.ts
# └── tsconfig.json
#
# 4 directories, 9 files
また、package.json
は以下のようになっている。
{
"name": "cdk-app",
"version": "0.1.0",
"bin": {
"cdk-app": "bin/cdk-app.js"
},
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"cdk": "cdk"
},
"devDependencies": {
"@types/jest": "^29.4.0",
"@types/node": "18.14.6",
"jest": "^29.5.0",
"ts-jest": "^29.0.5",
"aws-cdk": "2.73.0",
"ts-node": "^10.9.1",
"typescript": "~4.9.5"
},
"dependencies": {
"aws-cdk-lib": "2.73.0",
"constructs": "^10.0.0",
"source-map-support": "^0.5.21"
}
}
次に、jest をアンインストールして vitest を導入する。
# jestのconfigを消去
rm jest.config.js
"scripts": {
// testコマンドをjestからvitestに切り替えておく
- "test": "jest",
+ "test": "vitest --run",
"devDependencies": {
// jest関係のパッケージをdevDepenciesから消しておく
- "@types/jest": "^29.4.0",
- "jest": "^29.5.0",
- "ts-jest": "^29.0.5",
vitest をインストールする
pnpm add -D vite vitest
次に vitest の config ファイルを作成する。
を参照しつつ、例えば以下のような感じで作る。
import 'vitest/config';
import { defineConfig, type UserConfig } from 'vite';
export default defineConfig({
test: {
globals: true, // describeやtest, it, expectなどをimport無しで使えるようにする
environment: 'node',
include: ['./test/**/*.(test|spec).ts'], // 元のjestの設定が`test/`以下のファイルを参照する設定だったので合わせている
},
});
これで、pnpm test
を実行すると
pnpm test
# RUN v0.29.8 /home/wsl-user/tmp/cdk-tmp/cdk-app
#
# ✓ test/cdk-app.test.ts (1)
#
# Test Files 1 passed (1)
# Tests 1 passed (1)
# Start at 00:08:35
# Duration 741ms (transform 46ms, setup 0ms, collect 12ms, tests 3ms)
のようにテストを自動で実行できる。
なお、vitest.config.ts
においてglobals: true
を設定することで import 無しで test を実行できるようにしているが、このままだとエディタなどが vitest の型を読み込めずにエラー表示が出たりするので、必要であればtsconfig.json
を以下のように設定する
// compilerOptionsで以下(types部分)を追記
"compilerOptions": {
+ "types": [
+ "vitest/globals"
+ ],
// ...
あるいは、各テストコードで
import { describe, test, expect, vi } from "vitest";
describe("***", () => {
test("test-case-***", () => {
// ...
});
});
のように、明示的にvitest
から import するようにする。
参考:
(2)snapshot テストの導入
などで言及されているように、cdk における有効なテスト手法として
- fine-grained assertions
- snapshot testing
が挙げられている。
前者は特に vitest 固有の話は出て来ない?と思われるので、後者について少し取り上げる。
snapshot テストを実行するには、test
ディレクトリ以下に例えば次のようなテストコードファイル(test/snapshot.test.ts
)を追加する。
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { CdkAppStack } from '../lib/cdk-app-stack';
test('snapshot test', () => {
const app = new cdk.App();
const stack = new CdkAppStack(app, 'TestStack');
const template = Template.fromStack(stack).toJSON();
expect(template).toMatchSnapshot();
});
これで
pnpm test
としてテストを実行すると snapshot テストが一緒に行われるようになり、__snapshots__
以下にスナップショット内容が自動で保存される。
snapshot の内容のアップデート時は
pnpm test -- -u
などのようにすれば良い。
ここまででプロジェクトのディレクトリ・ファイル構成は以下のような感じ:
tree --gitignore
# .
# ├── README.md
# ├── bin
# │ └── cdk-app.ts
# ├── cdk.json
# ├── lib
# │ └── cdk-app-stack.ts
# ├── package.json
# ├── pnpm-lock.yaml
# ├── test
# │ ├── __snapshots__ ★自動生成
# │ │ └── snapshot.test.ts.snap ★自動生成
# │ ├── cdk-app.test.ts
# │ └── snapshot.test.ts ★追加
# ├── tsconfig.json
# └── vitest.config.ts
#
# 5 directories, 11 files
(...とここまでは別に vitest 固有の話は出て来ないが、以降の手順で vitest 特有の対処が必要な箇所が出てくるので順次紹介する)
asset(Lambda)の追加
esbuild でコンパイルが必要な asset として Lambda を追加し、cdk で管理するようにする。
簡単な例として、
pnpm add -D @types/aws-lambda esbuild
によりトランスパイル用の esbuild と Lambda 用の型定義をインストールしておき、その上で、
ディレクトリlambda
を作成し、その下にhello.ts
などの名前で Lambda のコードを追加する。
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
export const lambdaHandler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
return {
statusCode: 200,
body: JSON.stringify({
message: "hello world",
}),
};
} catch (err) {
console.log(err);
return {
statusCode: 500,
body: JSON.stringify({
message: "some error happened",
}),
};
}
};
次に、lib/cdk-app-stack.ts
を編集して Lambda をテンプレートに含める
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';
import { // 追加
aws_lambda as lambda,
} from 'aws-cdk-lib';
import { // 追加
NodejsFunction,
OutputFormat,
type NodejsFunctionProps,
} from 'aws-cdk-lib/aws-lambda-nodejs';
export class CdkAppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// The code that defines your stack goes here
// example resource
// const queue = new sqs.Queue(this, 'CdkAppQueue', {
// visibilityTimeout: cdk.Duration.seconds(300)
// });
// 以下のlambdaに関する部分を追加
const sampleLambda = new NodejsFunction(this, 'test-lambda', {
entry: './lambda/hello.ts',
handler: 'lambdaHandler',
runtime: lambda.Runtime.NODEJS_18_X,
});
}
}
これで
pnpm cdk synth
などとすれば Lambda のトランスパイル・アセット生成とテンプレート生成が完了する。
ただし、vitest を使う場合だと snapshot テストの際に esbuild でテンプレートを生成する部分がテスト中で失敗する。
などを見ると、vitest 実行時に thread を作らないようにvitestのデフォルトのスレッドプール(tinypoolを使用しているとのこと)を使わないようにすれば良さそう(2024/2/21修正)なので、
vitest.config.ts
に以下の設定(pool: 'forks'
)を追加しておく。
import 'vitest/config';
import { defineConfig, type UserConfig } from 'vite';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['./test/**/*.(test|spec).ts'],
+ pool: 'forks', // 追加
// threads: false, // 追加 <- (vitestのアップデートに伴い、2024/2/21削除)
},
});
これでvitest
の snapshot test 実行時におけるエラーが解消される。
snapshot test における asset の変更の無視
インフラ構成は変えていないが、lambda など asset の内容を変えた際に snapshot test の実行結果が変わるのを避けたい場合が存在する。
と同じこと(serializer を導入し、asset の hash 値を固定化する)をやれば OK だが、
vitest では全く同じようにはいかないので特有の対応を行う必要がある。
まずは ↑ の記事を参考にtest/plugins/ignore-asset-hash.ts
を作成する。
export const ignoreAssetHashSerializer = {
test: (val: unknown) => typeof val === 'string',
serialize: (val: string) => {
return `"${val.replace(/([A-Fa-f0-9]{64}.zip)/, 'HASH-REPLACED.zip')}"`;
},
};
Developers IO の記事では、
jest を使っているためjest.config.js
を編集して上記のプラグインをsnapshotSerializer
に登録しているが、
今回は vitest を使っているため、異なる方法で登録する。
具体的には、snapshot test のコード test/snapshot.test.ts
に以下のように追記する。
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { CdkAppStack } from '../lib/cdk-app-stack';
import { ignoreAssetHashSerializer } from './plugins/ignore-asset-hash'; // ★追加
test('snapshot test', () => {
const app = new cdk.App();
const stack = new CdkAppStack(app, 'TestStack');
const template = Template.fromStack(stack).toJSON();
// 次の行でプラグインをsnapshotSerializerに登録する
expect.addSnapshotSerializer(ignoreAssetHashSerializer); // ★追加
expect(template).toMatchSnapshot();
});
これで
pnpm test -- -u
などとすると snapshot test の結果がアップデートされ、今回の Lambda のトランスパイル結果に相当するアセット部分だと
// snapshottestの実行結果(Lambdaのasssetに関する部分)
- "S3Key": "c35f1ac5d9b1bb43826a97c396fd7852dd4333c9414f1eaa527f5a0cd4e1994d.zip",
+ "S3Key": "HASH-REPLACED.zip",
のような形で、hash 値が固定文字列に置き換わっており、
Lambda のソースコードが書き換わっただけでは snapshot test の実行結果が変わらないようになった。
まとめ
最終的なディレクトリ・ファイル構成は以下のような感じ
tree --gitignore
# .
# ├── README.md
# ├── bin
# │ └── cdk-app.ts
# ├── cdk.json
# ├── lambda # ★ 追加
# │ └── hello.ts
# ├── lib
# │ └── cdk-app-stack.ts # ★編集: ランタイムがNodejsで動作するTypeScriptで記述されたLambdaのリソースを追加
# ├── package.json # packageのアンインストール・インストール + テスト実行のためのnpm-scriptsを編集
# ├── pnpm-lock.yaml # lockfile
# ├── test
# │ ├── __snapshots__
# │ │ └── snapshot.test.ts.snap # ★機械的に追加、アップデート
# │ ├── cdk-app.test.ts
# │ ├── plugins
# │ │ └── ignore-asset-hash.ts # ★追加
# │ └── snapshot.test.ts
# ├── tsconfig.json # ★編集: vitestの型情報を読み込む設定を追記
# └── vitest.config.ts # ★追加
#
# 7 directories, 13 files
微妙に設定が必要な箇所はあったが、jest を使う場合とほとんど同じように設定・実行することができた。
ただし、vitest における設定: threadsを false に設定しているため、
テストファイルが大量に必要なケースではパフォーマンス的に良くなかったり、テスト間の環境分離が不十分になることに起因する副作用だったりが起こる可能性もある。
そのような場合、若干面倒だが snapshot test だけ分離・独立させて行うのが良さそう。
(vitest cli のオプション: -c
でコンフィグを別途指定できるので、snapshot test だけコンフィグファイルを分けるとか、あるいはvitest.config.ts
内でうまく処理分岐をさせるなどの方法が考えられる)
(2024/2/21追記)
vitestのテスト並列実行方法の制御オプションがv1で追加されているため、vitestを使うとテストを並列実行できない問題は無くなっている。(該当箇所の記述は変更済み)
このとき、デフォルトの挙動から変更してnodeのchild_processを使ったプロセスのフォークによってテストの並列実行がなされる。
また、追記時点において、viteでcjs形式のbuildがdeprecatedになっているため、この記事で記載したやり方で作ったCDKプロジェクトでvitestを使うとwarningが出るようになる。(v6からはおそらくErrorになる)
そのため、プロジェクト全体をES Module形式に修正するなどの必要がある。
Discussion