この記事はNTTテクノクロス Advent Calendar 2024シリーズ2の24日目の記事であり、翌日25日の記事と合わせた連載の第1回目になります。
こんにちは。NTTテクノクロスの堀江です。普段はAWSやAzure上でのシステム設計、構築や実装、調査検証系の案件を幅広く担当しています。
1. はじめに
本記事では、「テストピラミッド」の概念を用いて、AWS CDKで実装するインフラの適切なテスト戦略を模索していきます。
テストピラミッドは一般的なソフトウェア開発におけるテスト戦略(どの様なテストを、どの程度実施すべきか)を検討する上で頻繫に用いられる概念です。それをインフラに対するテスト戦略としても運用可能か、インフラのテストにおけるテストピラミッドはどの様な形になるか、といったことを本記事では模索します。
本記事を執筆した背景については、長くなるのでこちらに記載
1.1. 構成
この取り組みは、本記事【理論編】と明日公開されるもう一つの記事【実践編】との2部構成となります。
1.1.1. 【理論編】
- はじめに、模索の前提となる幾つかの情報・用語を棚卸しします
- まずは、AWS CDKで実装したインフラに対してテストを実装するために利用可能なモジュールやライブラリを紹介します
- 次に、一般的なソフトウェア開発におけるテストのカテゴリとして 「ユニットテスト」「インテグレーションテスト」「E2Eテスト」 を紹介し、本記事内でのそれらの定義づけを行います
- 必要な前提用語が出揃ったら、次に一般的なソフトウェア開発におけるテスト戦略の大きな指針となる「テストピラミッド」の考えを紹介します
- 一般的なソフトウェア開発におけるテストピラミッドの概要と、そのピラミッド上では各種テストカテゴリがどの様な性質を持っているとされるのかを整理します
- そのうえで、インフラにおける各種テストカテゴリの具体的な実装方法とそれらの性質を明らかにし、テストピラミッドの考えに適合するかを検討します
- ここでの主な結論の一つとして、インフラにおける「ユニットテスト」は一般的なソフトウェア開発における「ユニットテスト」とは異なり、単純にテストピラミッドの考えを流用出来ないことを示します
- そして、「ユニットテスト」というテストカテゴリに代わるより適切なインフラのテストカテゴリの1つとして、「コンパイルテスト」というカテゴリを導入します
- またそれに関連して、インフラにおける「ユニットテスト」と「インテグレーションテスト」というカテゴリに取って代わる、「機能テスト」というより適切なカテゴリも新たに導入します
- 最後に、「コンパイルテスト」「機能テスト」「E2Eテスト」から成るインフラの適切なテストピラミッドの図を描画し、そこから得られる教訓を示します
1.1.2. 【実践編】
- シンプルなWEBアプリケーションのCDKコードを題材に各種テストを実装することを通して、理論編で整理したテスト戦略を実践します
- コンパイルテスト(きめ細かなアサーションテスト)では、効果的なテスト観点も実装例と併せて検討します
- 機能テストでは、一般的なソフトウェア開発におけるプラクティス(デザインパターンやSOLID原則)を意識することでメリットが得られる可能性を示します
1.2. 補足・注意事項
- 【理論編】【実践編】共に、扱うコードはTypeScriptとします
- 本取り組みで述べられる「インフラ」は、基本的にAWS CDKで実装され、AWS上に構築された(される)インフラのことを意味します
- 本記事で述べるのは非常に模索的な内容です。つまり、長年の経験や大人数のフィードバックを経て確立され成熟した理論やプラクティスを紹介するものではありません。私個人の極めて模索的な内容となります
2. 前提情報の棚卸し
2.1. AWS CDKにおけるテストの実装手法
ここでは、CDKで実装したインフラに対してテストを実装するために選択肢となるモジュールやライブラリを簡単に紹介します。ここで紹介したテスト手法を、最終的にテストピラミッド上にマッピングしていきます。
なお、ここではサードパーティーが提唱・開発しているライブラリは基本的に取り上げず、可能な限りAWSの公式に近いものにフォーカスします。
2.1.1. きめ細かなアサーションテスト
きめ細かなアサーションテストは、CDKコードから生成されたCFNテンプレート上のリソースやアウトプットに、期待通りの値が設定されているかをテストする手法です。aws-cdk-libのassertionsモジュールにて提供されています。
きめ細かなアサーションテストの例
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", {
maxAzs: 4,
subnetConfiguration: [
{ subnetType: ec2.SubnetType.PUBLIC, name: "Public", cidrMask: 24 },
],
});
const template = Template.fromStack(stack);
test("サブネットが合計2つ作成される", () => {
template.resourceCountIs("AWS::EC2::Subnet", 2);
});
きめ細かなアサーションテストを効率的かつ高速に実装するライブラリとして、aws-cdk-utulをリリースしました。
興味のある方はこちらのアドベントカレンダー記事をご確認ください。
2.1.2. スナップショットテスト
スナップショットテストは、CDKコードから生成されたCFNテンプレートを以前のもの(スナップショット)と比較し、差分の有無を確認するためのテストです。スナップショットテスト自体はHTMLページといったUIコンポーネントの差分をテストする概念としても一般的であり、jestの機能として提供されています。
スナップショットの例
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", {
maxAzs: 4,
subnetConfiguration: [
// 一度cidrMask: 24でスナップショットを作成し、その後cidrMask: 26に更新する
// { subnetType: ec2.SubnetType.PUBLIC, name: "Public", cidrMask: 24 },
{ subnetType: ec2.SubnetType.PUBLIC, name: "Public", cidrMask: 26 },
],
});
test("スナップショットテスト", () => {
expect(template).toMatchSnapshot();
});
$ npm test test/unit/snapshot.test.ts
... 省略 ...
● スナップショットテスト
expect(received).toMatchSnapshot()
Snapshot name: `スナップショットテスト 1`
- Snapshot - 2
+ Received + 2
@@ -81,11 +81,11 @@
{
"Fn::GetAZs": "",
},
],
},
- "CidrBlock": "10.0.0.0/24",
+ "CidrBlock": "10.0.0.0/26",
"MapPublicIpOnLaunch": true,
"Tags": [
{
"Key": "aws-cdk:subnet-name",
"Value": "Public",
@@ -153,11 +153,11 @@
{
"Fn::GetAZs": "",
},
],
},
- "CidrBlock": "10.0.1.0/24",
+ "CidrBlock": "10.0.0.64/26",
"MapPublicIpOnLaunch": true,
"Tags": [
{
"Key": "aws-cdk:subnet-name",
"Value": "Public",
16 |
17 | test("スナップショットテスト", () => {
> 18 | expect(template).toMatchSnapshot();
| ^
19 | });
20 |
2.1.3. integ-tests
integ-testsはCDKコードからデプロイされたCFNスタックに対して、自動化されたインテグレーションテスト(結合テスト)を実行する為のモジュールです。
下図のように、テスト対象となる一連のリソース群とテストシナリオをCFNスタックとして定義・デプロイする形でインテグレーションテストを実行することが可能です。
integ-testsモジュールは2024年11月時点ではまだアルファ版のみが利用可能です。各自で利用される際はその点をご留意ください。また、本記事の記載内容と今後の更新内容が大きく乖離する可能性にご注意ください。
integ-testsの例
import { ExpectedResult, IntegTest } from "@aws-cdk/integ-tests-alpha";
import { App, RemovalPolicy, Stack, aws_s3 as s3 } from "aws-cdk-lib";
import { PutObjectCommandInput } from "@aws-sdk/client-s3";
const app = new App();
// インテグレーションテスト対象とする一連のリソース群をスタックとして定義
// パブリックアクセス可能なS3バケットを作成する
const stack = new Stack(app, "stack-under-test");
const publicBucket = new s3.Bucket(stack, "PublicBucket", {
publicReadAccess: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS,
removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true,
});
const objectKey = "test-object";
const objectBody = "test";
// 上で定義したスタックを対象とするテストケースを定義
const integ = new IntegTest(app, "test-bucket-public-access", {
testCases: [stack],
});
// テスト用のオブジェクトをアップロートし、パブリックアクセス(GET)可能であることをテストする
integ.assertions
.awsApiCall("@aws-sdk/client-s3", "PutObject", {
Bucket: publicBucket.bucketName,
Key: objectKey,
Body: objectBody,
} as PutObjectCommandInput)
.next(
integ.assertions.httpApiCall(
`https://${publicBucket.bucketName}.s3.${stack.region}.amazonaws.com/${objectKey}`
)
)
.expect(ExpectedResult.objectLike({ body: objectBody }));
$ npx integ-runner --directory test/integration/ --parallel-regions ap-northeast-1 --update-on-failed
...省略...
Running integration tests for failed tests...
Running in parallel across regions: ap-northeast-1
Running test /home/horie-t/workspace/advent-calendar-2024/test/integration/tmp/integ.app.ts in ap-northeast-1
SUCCESS integ.app-test-bucket-public-access/DefaultTest 164.331s
AssertionResultsHttpApiCall153ddd9a4762d5bdcdd63b9d9d489185 - success
Test Results:
Tests: 1 passed, 1 total
2.1.4. CDKパイプラインステージ
AWS CDKではCDK Pipelineモジュールを利用することで、CDKコードのCI/CDを効率的に実現することが可能です。その処理の一環として、CDKコードのデプロイステージの前後に自動テストを実行するステージを追加することが可能です。
これによって、AWS上にデプロイされたインフラに対して自動化されたテストを実施することが可能です。
デプロイされたリソースに対するテストステージの実装例
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines';
import { MyPipelineAppStage } from './my-pipeline-app-stage';
export class MyPipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// CI/CDパイプラインの定義
const pipeline = new CodePipeline(this, 'Pipeline', {
pipelineName: 'MyPipeline',
synth: new ShellStep('Synth', {
input: CodePipelineSource.gitHub('OWNER/REPO', 'main'),
commands: ['npm ci', 'npm run build', 'npx cdk synth']
})
});
// アプリケーションスタックをデプロイするステージ
const deployStage = pipeline.addStage(new MyPipelineAppStage(this, "test", {
env: { account: "111111111111", region: "eu-west-1" }
}));
// デプロイされたアプリケーションに対するテストステージを追加
deployStage.addPost(new ShellStep("validate", {
commands: ['../tests/validate.sh'],
}));
}
}
2.1.5. その他
CDKコードのテスト手法として他にも以下のような選択肢を挙げられますが、後述するテストピラミッドとの対比関係を検討していく上ではあまり関係しないため、本記事では基本的に取り上げません。
- AspectsやaddValidation、Annotations機能を利用した合成時のバリデーション
- cfn-nagやCloudFormation Guardといったツールを利用したセキュリティ設定、コンプライアンス設定チェック
2.1.6. 各種CDKテスト手法のまとめ
テスト手法 | 概要 |
---|---|
きめ細かなアサーションテスト | CDKコードから期待通りにCFNテンプレートが生成されることを確認するテスト |
スナップショットテスト | CFNテンプレートに意図しない変更が発生しないこと(或いは発生した変更が意図した内容であること)を確認するテスト |
integ-tests | テスト対象のリソースをAWS上にデプロイして期待通りに動作することを確認するテスト |
CDKパイプラインステージ | CI/CDパイプラインの処理の一環で、デプロイされたリソースが期待通りに動作することを確認するテスト |
2.2. 一般的なソフトウェア開発におけるテストカテゴリ
次に、一般的なソフトウェア開発におけるテストカテゴリ、及び本記事におけるそれらの定義を紹介します。
一般的なソフトウェア開発におけるテストをカテゴライズするための用語には様々なものがありますが、本記事ではその中から下記3種類のテストカテゴリを取り上げます。
-
ユニットテスト
- ソフトウェアを構成するコンポーネントの最小単位(関数やメソッド等)の振る舞いに対するテスト
-
インテグレーションテスト
- 複数のコンポーネント同士の連携や、DB等の外部リソースへのアクセスを伴う、より大きな纏まり(ユースケースレベル)の振る舞いに対するテスト
-
E2Eテスト
- エンドユーザに利用されるのと同じ状態のソフトウェア(≒製品としてデプロイされた状態のソフトウェア)に対して、エンドユーザが利用するのと同じ方法(インターフェース)で利用した際の振る舞いに対するテスト
これらのテストカテゴリの定義は人によって様々かと思います。また、例えばインテグレーションテストは「サービステスト」、E2Eテストは「UIテスト」といった表現もあり得ますが、本記事では上記の用語・定義を一貫して使用します。
3. テストピラミッドを用いた考察
ここまで、テスト戦略を検討するうえで前提となる用語を棚卸ししてきました。それらの情報と、「テストピラミッド」の概念を用いて、インフラに対するテストピラミッドを考察します。
3.1. 一般的なソフトウェア開発におけるテストピラミッド
テストピラミッドはMike Cohn氏氏が2009年の書籍「Succeeding with Agile」で提唱した概念です1。ソフトウェア開発におけるユニットテスト・インテグレーションテスト・E2Eテスト(Mike氏の分類では「UIテスト」)を以下の4観点で分析し、それぞれどの程度の割合で実施すべきなのかをについて重要な指針を提供します。
- テストに掛かるコスト(テストケースの記述コストと実行時のコスト)
- 記述にコストが掛かるテストは、それだけ開発やリリースの速度を低下させる。実行時のコスト(マシンリソース使用率や金銭)が高いテストも、テストの実行を躊躇わせる要因となる
- テスト時のコンポーネントの振る舞いの忠実度
- テスト時のコンポーネントの挙動や実行条件が、本番利用時のそれらにどれだけ近いかの度合い
- テスト時に実行されるコンポーネントのスコープが限定されていたり、テストダブルやダミーデータ、インメモリDBを使用したテストであるほど振る舞いの忠実度は低くなる
- 忠実度の高い条件下でテスト出来るほど、そのソフトウェア本来の動作をテスト出来ていると言えるので、ソフトウェアの信頼性は高まる
- テストの実行速度
- テストを実行してから終了(結果が判明)するまでに掛かる時間
- 実行時間が長ければ、それだけ開発やリソースのボトルネックとなる
- テストの決定性
- 同じ条件で同じ内容のテストを複数回実行した際に、毎回必ず同じ結果が得られる可能性
- 外的な要因(インターネット通信や外部サービスとの連携)が関係し、時間が掛かるテストほど決定性は下がる
- 決定性が低いテストはテストそのものの信頼性を下げ、ソフトウェアの品質にも自信が持てなくなる要因となる
和田卓人氏の以下の説明2が、より端的に分かり易いので、併せて引用させていただきます。
テストピラミッドとは、コスト(記述コストと実行コスト)と忠実性(本物の挙動を反映している度合い)が高く、実行速度と決定性(テストが毎回同じように安定して動く度合い)が低いテストほどケース数を減らすべきだという、自動テストケース数の望ましい比率をピラミッド型に視覚化したものです。
テストピラミッドは以下の様な図3で表現出来ます。
この図が意味するところは以下の通りです。
- コストが低くて実行速度が速く、決定性が高い代わりに忠実度が低いテストは手厚く実装すべき。(ピラミッドの最下部を構成するイメージ)
- ※一般にユニットテストがこの性質に該当する
- コストが高くて実行速度が遅く、決定性が低い代わりに忠実度が高いテストは実装数を抑えるべき。(ピラミッドの最上部を構成するイメージ)
- ※一般にE2Eテストがこの性質に該当する
- コストや実行速度、決定性が上記2つの中間的なテストは、上記2つの中間を占める程度の実装数とすべき。(ピラミッドの中段を構成するイメージ)
- ※一般にインテグレーションテストがこの性質に該当する
- ピラミッドの下部を構成するテスト(ユニットテスト)でソフトウェアの大部分(コンポーネントの多種多様な挙動)のテストをカバーできるようなソフトウェアの実装を目指すべき(適切に設計やリファクタリングを行う等して)
- かといって、テストすべき全ての振る舞いをユニットテストとしてテスト出来るわけでは無く、E2Eテストやインテグレーションテストも(数は抑えるべきだが)実施が不可欠である
上図のような割合でテストを実装することで、ソフトウェアの開発速度と品質(信頼性)を高いレベルで両立することが出来ると一般的に考えられています。
テストピラミッドはあくまで一般的なソフトウェア開発に対するテスト戦略の指針として用いられています。では次に、この考えをCDKインフラ開発におけるテスト戦略にも流用することが出来るのかを考察します。
3.2. CDKインフラ開発におけるテストピラミッド
前述したテストピラミッドの考え、及びそこから導き出されるテスト戦略を、CDKインフラ開発にも流用することが出来るのかを考えます。
アプローチは以下の通りです。
- まずは、前提として挙げた3つのテストカテゴリ(ユニットテスト、インテグレーションテスト、E2Eテスト)がCDKインフラ開発において具体的にどの様なテストを意味するのか検討します
- 次に、それらのテストの、実行速度やコストといった性質(テストピラミッド上の位置づけ)を検討します
- そして、CDKインフラ開発における各テストの性質(テストピラミッド上の位置づけ)が、一般的なソフトウェア開発におけるそれと同様であるかを確認します
- 例えばCDKインフラ開発における「ユニットテスト」が、一般的なソフトウェア開発におけるユニットテストとコストや速度の面で同様の性質なのであれば、一般的なソフトウェア開発におけるテストピラミッドの教訓(ユニットテストを沢山実施すべき)をインフラ開発においても流用出来ると考えます
3.2.1. ユニットテスト(コンパイルテスト)
本記事におけるユニットテストの定義と、テストピラミッドにおけるユニットテストの性質は以下の通りでした。
定義 | 性質 |
---|---|
コンポーネントの最小単位の振る舞いに対するテスト | コストが低くて実行速度が早く、決定性が高い代わりに忠実度が低い |
この定義に基づくCDKインフラ開発における「ユニットテスト」とはどの様なテストを意味するのかを考えるために、まずはこの定義の、「コンポーネントの最小単位」と「振る舞い」をもう少し掘り下げて考えます。
一般的なソフトウェア開発における「コンポーネントの最小単位」とはコードとして記述された関数やメソッドだと述べました。そしてその「振る舞い」とは、実際にそのコードが実行された際に行われる処理と言えます。そして1つ以上の振る舞いの結果として、最終的にユースケースが実現されます。
では、インフラにおいて最終的に実現したいユースケースを構成する「コンポーネントの最小単位」と、その「振る舞い」とは何でしょうか。それはユースケースに関わる各種AWSリソース(やAWSリソース上で稼働するコード)であり、「振る舞い」はそれらがAWS上で動作することに他なりません。
つまり、上述の定義に則ったCDKインフラ開発における「ユニットテスト」とは、「AWSリソース個別の、AWS上での動作をテストすること」という定義になります。では、そのようなテストがテストピラミッドの最下層を構成するテストの「コストが低くて実行速度が速く、決定性が高い代わりに忠実度が低い」という性質を満たしているのでしょうか。答えは基本的にNoでしょう。
テストピラミッドの4観点 | AWSリソースの動作試験の特徴 |
---|---|
コスト | リソースの実行には直接的に金銭コストも掛かる。またテストダブルの用意にも手間が掛かり、記述コストも高い |
速度 | リソースの作成・削除は数秒~数十分掛かる。長時間を要するテストケースも想定される。 |
決定性 | AWSリソースの操作はパブリックなNWを経由する必要があり、AWSサービス自体の更新や障害の影響を受けるため、決定性は低い。 |
忠実度 | 実際にAWS上で動作する通りの動作をテストするので、忠実度は高い |
このことから、CDKインフラ開発における「ユニットテスト」の定義に則ったテストは、一般的なソフトウェア開発における少なくともインテグレーションテストに相当する性質を有しているといえます。言い換えれば、CDKインフラ開発における「ユニットテストは、一般的なソフトウェア開発におけるユニットテストのような手軽さで実施できるものではない、とも言えます。
では逆に、一旦ユニットテストの定義は無視し、テストピラミッドの最下層を構成するような、手軽に実行出来る性質を持ったテストが、CDKインフラ開発におけるテストとして無いか逆引きしてみましょう。前述したテスト手法の中では、「きめ細かなアサーションテスト」と「スナップショットテスト」がそれに該当すると言えます。これらのテストはローカルマシン上のCDKコードを対象とするテストであるため、高速且つ低コストで実施可能です。ただし、AWS上のAWSリソースの振る舞いを実際にテストしている訳では無いので、忠実度は非常に低いと言えます。
テストピラミッドの4観点 | きめ細かなアサーションテストとスナップショットテストの特徴 |
---|---|
コスト | ソフトウェア開発におけるユニットテストと同等の形式で実装でき、実行に必要なリソースも少ない |
速度 | 数秒~1分程度で実行可能 |
決定性 | 基本的にローカルマシン上で完結する(依存する外部リソースが無い)ため、決定性は高い |
忠実度 | 実際のAWS上のAWSリソースの動作を試験している訳では無いので、忠実度は(かなり)低い |
これらのテストは「ユニットテスト」と明確に区別して扱う為に、本記事では以下のようなカテゴリと定義を与えたいと思います。
-
コンパイルテスト
- コンポーネントの生成に関する振る舞いに対するテスト
- 高速且つ低コストで決定性が高い代わりに、実際のコンポーネントの振る舞いを対象にしている訳では無いので、忠実度は非常に低い
特にAWS CDKにおいては、「コンポーネントの生成に関するテスト」を表現する言葉として、「synthesize test(合成テスト)」という言葉がより実態に即した言葉だとは思いますが、ソフトウェアのE2Eな振る舞いを継続的に監視するための「synthetic test(synthetic monitoring)」という用語が市中に存在しているため、ここでは「コンパイルテスト」という表現を採用します。
少し長くなってしまったので、図も交えてここまでの内容を纏めます。
- インフラにおいて、「コンポーネントの最小単位の振る舞い」に対するユニットテストは、実際にAWS上のリソースの動作をテストすることに相当します。しかしその類のテストは、「コストが低くて実行速度が速く、決定性が高い代わりに忠実度が低い」という、テストピラミッドの下部を構成するための特徴を伴いません
- 逆に、「コストが低くて実行速度が速く、決定性が高い代わりに忠実度が低い」という特徴を伴う、CDKコードで利用可能なテスト手法として、きめ細かなアサーションテストやスナップショットテストが該当します。しかしこれらは本来テストしたい対象をテスト出来ている訳では無いので、ユニットテストの定義を満たしません
- 後者のようなテストは「コンパイルテスト」と定義して、前者と明確に区別します
以上から、一般的なソフトウェア開発におけるテストピラミッドが示す教訓(ユニットテストを沢山実施すべき)は、CDKインフラ開発にはそのまま当てはめることが出来ず、以下のように改訂する必要があります。
- (コンポーネントの生成に関する振る舞いを対象とした)コンパイルテストを、中程度実施すべき
- 何故ならコンポーネントの生成に関する振る舞いは、最終的にテストしたいAWSリソースの振る舞いとは根本的に異なり、テストの効果は限定的であるため
3.2.2. インテグレーションテスト(機能テスト)
本記事におけるインテグレーションテストの定義と、テストピラミッドにおけるインテグレーションテストの性質は以下の通りでした。
定義 | 性質 |
---|---|
複数のコンポーネント同士の連携や外部リソースへのアクセスを伴う、ユースケースレベルの振る舞いに対するテスト | コスト・実行速度・決定性・忠実度共に中程度 |
前章にて、CDKインフラ開発における「ユニットテスト」はAWSリソースの動作に対するテストであり、一般的なソフトウェア開発におけるインテグレーションテスト相当の性質を持っているものと解釈しました。CDKにおけるユニットテストもインテグレーションテストも、テスト対象は(一つ以上の)AWSリソースのAWS上での実際の振る舞いであり、その性質(コストや決定性)も似ていることを考えると、両者の境界は非常に曖昧になっていきます。「インテグレーションテスト」という表現はある意味「ユニットテスト」との対比・差異を強調するための表現でもあると考えれば、両者の境界が曖昧な今、CDKインフラ開発におけるユニットテストとインテグレーションテストを包括したより適切な 「機能テスト」 というテストカテゴリをここで追加したいと思います。
-
機能テスト
- 一つ以上のAWSリソースの振る舞いやAWSリソース間の連携、外部リソースへのアクセスを伴う、ユースケースレベルの振る舞いに対するテスト
- 例えば「CloudWatchアラームからSNSトピックを経由してアラーム情報をメール送信する」といったような、インフラ観点のユースケースに対するテスト
- コスト・実行速度・決定性・忠実度共にテストケースごとに分散が大きいが平均して中程度。また、テストケースやCDKコードの実装を工夫することである程度は改善可能
- 一つ以上のAWSリソースの振る舞いやAWSリソース間の連携、外部リソースへのアクセスを伴う、ユースケースレベルの振る舞いに対するテスト
では、CDKインフラ開発におけるインテグレーションテスト、もとい機能テストは、テストピラミッドにおけるインテグレーションテスト相当の性質を持っていると言えるでしょうか?条件付きではありますが、概ねYesだと言えるでしょう。
テストピラミッドの4観点 | インフラ観点のユースケースに対するテストの性質 |
---|---|
コスト | リソースの実行には直接的に金銭コストも掛かる。またテストダブルの用意にも手間が掛かり、記述コストも高い。ただし後述のようにテストケースを工夫することある程度まで改善可能 |
速度 | リソースの作成・削除は数秒~数十分掛かる。テストの所要時間はテストケースに依る(数秒~数日)。ただしコストと同様に工夫次第で改善可能 |
決定性 | AWSリソースの操作はパブリックなNWを経由する必要があり、AWSサービス自体の更新や障害の影響を受けるため、決定性は低い。 |
忠実度 | 実際にAWS上で動作する通りの動作をテストするので、忠実度は高い。ただしダミーデータやダミーのアプリケーション、本来よりも小さいスペックのリソースを使用する場合はその分忠実度は下がる。 |
機能テストの実行速度やコストを改善するには、以下のようなテストケース及びCDKコードの工夫が重要です。
- テストの度にインフラを構成する全てのリソースのをデプロイするのではなく、テストケースごとに必要最小限のリソースのみを、必要最小限のスペックでデプロイしてテストすることでコストや実行時間を節約する
- インフラ上で稼働するアプリケーションやデータベースのデータがテストに必要な場合は、テストが容易なようにダミーのアプリやデータを使用して、テストケースをシンプルにする
- 一部のリソースを代替可能な別のリソース(≒テストダブル)に置き換えてテストすることで、コストや実行速度を削減する
CDKコードに対して機能テストを実施するためのテスト手法としては、「integ-tests」が該当します。デプロイすべきコンポーネントをCDKコンストラクトとして実装し、必要最小限のコンストラクトをテスト用のスタックとして一時的にデプロイすることで、コストや実行速度を最低限に保ちながらAWSリソースの実際の振る舞い、そしてユースケースをテストすることができるようになります。
以上から、一般的なソフトウェア開発におけるテストピラミッドの教訓(インテグレーションテストを、中程度実施すべき)は以下のように改訂すべきです。
- 一つ以上のAWSリソースやリソース間の連携、外部リソースへのアクセスを伴う、ユースケースレベルの振る舞いを対象とした機能テストを、中程度実施すべき
- その際実行速度やコストを最適化出来るようテストケースやCDKコードの実装を工夫すべき
3.2.3. E2Eテスト
本記事におけるE2Eテストの定義と、テストピラミッドにおけるE2Eテストの性質は以下の通りでした。
定義 | 性質 |
---|---|
エンドユーザに利用されるのと同じ状態のソフトウェアに対して、エンドユーザが利用するのと同じインターフェースで利用した際の振る舞いに対するテスト | コストは高く、実行速度と決定性が低いが、忠実度は高い |
インフラにおける「エンドユーザ」は、インフラ上で稼働されるアプリケーションやデータベース、あるいは運用者と解釈するのが適切でしょう。なのでインフラにおけるE2Eテストとは例えば「インフラ上で稼働するWEB3層アプリケーションがクライアントからのリクエストに期待通りのレスポンスを返すこと」や「インフラを構成する特定コンポーネントの異常時に通知が運用者に届くこと」を確認するためのテストとなります。これらのテストはインフラを構成するリソースが全て揃った状態、かつ、テストダブル(ダミーのアプリやAWSリソース)を使用せず実施する必要があります。
テストピラミッドの4観点 | インフラ観点のユースケースに対するテストの性質 |
---|---|
コスト | インフラを構成する全てのリソースがデプロイされている状態なので、その分の金銭的コストが掛かる。 |
速度 | リソースの作成・削除は数秒~数十分掛かる。テストの所要時間はテストケースに依る(数秒~数日) |
決定性 | AWSリソースの操作はパブリックなNWを経由する必要があり、AWSサービス自体の更新や障害の影響を受ける。また、インフラ上で稼働させるアプリケーションやDBデータもダミーは使用できないため、決定性は低い。 |
忠実度 | 実際のインフラの使われ方と(限りなく)同じ方法でテストするので、忠実度は非常に高い。 |
この条件のインフラに対して実施可能なテスト手法は、「CDKパイプラインステージ」が考えられます。前述の通り、インフラを構成する全てのリソースがデプロイされている状態に加え、インフラ上で稼働するアプリケーションやDBデータも正規のものである必要があります。そのため、CI/CDパイプラインの処理としてデプロイが成功したインフラ全体に対してテスト出来るこの手法が適切であると言えます。
以上から、一般的なソフトウェア開発におけるテストピラミッドの教訓(E2Eテストを、少数実施すべき)は、そのまま流用出来ると言えます。
4. まとめ
ここまで、テストピラミッドを構成する各種テストカテゴリの定義に則り、該当するCDKコードのテスト手法を整理し、そのテスト手法がテストピラミッドを構成しえるかを検討してきました。その結果を今一度まとめます。
テストカテゴリ | 定義 | テストピラミッド上の性質 | CDKテスト手法 |
---|---|---|---|
コンパイルテスト | コンポーネントの生成に関する振る舞いに対するテスト | 低コストで高速で決定性は非常に高いが、忠実度は非常に低い | きめ細かなアサーションテスト(及びスナップショットテスト) |
機能テスト | 一つ以上のAWSリソースの振る舞いやAWSリソース間の連携、外部リソースへのアクセスを伴う、ユースケースレベルの振る舞いに対するテスト | (テストケースにも依るが、)忠実度は高い代わりにコストも速度もそれなりに掛かり、決定性も高くない。ただしCDKコードやテストケースの実装の工夫次第で改善可能。 | integ-tests |
E2Eテスト | エンドユーザに利用されるのと同じ方法で利用した際の振る舞いに対するテスト | 忠実度が非常に高いが、一般に高コストかつ低速で、決定性も高くない。 | CDKパイプラインステージ |
以上から、インフラにおけるテストピラミッド及びそこから得られる教訓は以下のようになります。
- コンパイルテストを実装し過ぎない。確かに高速かつ低コストで実行できるテストであるが、コンパイルテストは実際にAWSリソースの振る舞いをテストできるわけではない
- 代わりに機能テストを厚く実装する。実際にAWSリソースの振る舞いをテストできるテストを、可能な限り高速かつ低コスト、高い決定性で実施できるようにテストケースやCDKコードの実装を工夫する
- E2Eテストの実装数は抑える。一般的なソフトウェア開発同様、E2Eテストはコスト、実行速度共に掛かるため、必要最低限に留める
本記事【理論編】は以上となります。次回【実践編】では、シンプルなWEBアプリケーションのCDKコードを題材に、実際にテストを実装・実施する一連の流れをデモンストレーションします。
4.1. この記事を書いた背景
私は今年度の業務から(遅ればせながら)AWS CDKを本格的に使い始め、その過程でこの取り組みを思いつきました。
AWS CDKでは本記事で紹介した通り、コード(コンストラクト)やAWS上のリソースに対して様々な自動テストを実施するためのモジュールが提供されています。それらのモジュールの使い方(各種テストの実装方法)は比較的容易に習得できたものの、「どのテストを、何を対象に、どの位実行すべきか」、といったテスト戦略を、(私のCDK利用経験が浅いこともあり)納得のいく形で確立し運用することが出来ませんでした。
結果的に、今年度業務内で実装したテスト及びそのプロセスは、生産性や品質の面であまり効果的では無いものとなりました。例えば、テストの実装とテストが成功するまでの試行錯誤がインフラのデプロイプロセス上の大きなボトルネックとなったり、振り返るとあまり意義の感じられないテストコードを大量に書いてしまっていたり、といった具合です。(勿論テストを実装したことで開発・運用が効率化された面や品質が向上したと実感出来る点も確かにありました。)
その様な経緯を経て、一般的なソフトウェア開発におけるテスト戦略を検討するうえで信頼できる指針である「テストピラミッド」の概念を用いて、AWS CDKで実装したインフラに対する適切なテスト戦略を模索し理論化するというこの記事の取り組みを思いつきました。
本記事の冒頭でも述べた通り、記事内で私が纏めた理論は私一人の今年度限りの業務経験と僅かな知識にのみ依っている、大変脆弱な内容となっています。そのため、今後の業務の中でこの理論を運用し、効果や欠点を検証していきたいと思います。
この取り組みが、同様の背景や悩みを持つエンジニアの一助になれば幸いです。
4.2. 参考文献
- Test AWS CDK applications
- Continuous integration and delivery (CI/CD) using CDK Pipelines
- AWS CDK アプリケーションのためのインテグレーションテストの作成と実行
- 単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略
- The Practical Test Pyramid
- サバンナ便り ~ソフトウェア開発の荒野を生き抜く~ 第5回 テストピラミッド ~自動テストの信頼性を中長期的に保つ最適なバランス~
- 自動テストにおけるテストピラミッド
- テストサイズで再考する「テストピラミッド」 Googleが提唱する効率的な自動テスト戦略
- Googleのソフトウェアエンジニアリング ―持続可能なプログラミングを支える技術、文化、プロセス