這是 AWS CDK v2 開發人員指南。較舊的 CDK v1 已於 2022 年 6 月 1 日進入維護,並於 2023 年 6 月 1 日結束支援。
本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。
透過 AWS CDK,您的基礎設施可以與您撰寫的任何其他程式碼一樣可測試。您可以在雲端和本機進行測試。本主題說明如何在雲端進行測試。如需本機測試的指引,請參閱使用 本機測試和建置 AWS CDK 應用程式 AWS SAMCLI。測試 AWS CDK 應用程式的標準方法使用 AWS CDK的聲明模組和常用的測試架構,例如 Jest
您可以為 AWS CDK 應用程式撰寫兩種測試類別。
-
精細聲明會測試所產生 AWS CloudFormation 範本的特定層面,例如「此資源具有此值的此屬性」。這些測試可以偵測迴歸。當您使用測試驅動的開發開發新功能時,它們也很有用。(您可以先撰寫測試,然後撰寫正確的實作,讓測試通過。) 精細聲明是最常使用的測試。
-
快照測試會根據先前存放的基準 AWS CloudFormation 範本來測試合成的範本。快照測試可讓您自由重構,因為您可以確定重構程式碼的運作方式與原始程式碼完全相同。如果變更是有意為之,您可以接受未來測試的新基準。不過,CDK 升級也可能導致合成範本變更,因此您無法僅依賴快照來確保您的實作正確。
注意
GitHub 上提供了
開始使用
為了說明如何撰寫這些測試,我們將建立包含 AWS Step Functions 狀態機器和 AWS Lambda 函數的堆疊。Lambda 函數會訂閱 Amazon SNS 主題,並直接將訊息轉送至狀態機器。
首先,使用 CDK Toolkit 建立空的 CDK 應用程式專案,並安裝我們需要的程式庫。我們將使用的建構都在主要 CDK 套件中,這是使用 CDK Toolkit 建立之專案的預設相依性。不過,您必須安裝測試架構。
$
mkdir state-machine && cd state-machine cdk init --language=typescript npm install --save-dev jest @types/jest
為您的測試建立目錄。
$
mkdir test
編輯專案的 package.json
以告知 NPM 如何執行 Jest,並告知 Jest 要收集哪些類型的檔案。必要的變更如下所示。
-
將新
test
金鑰新增至scripts
區段 -
將 Jest 及其類型新增至
devDependencies
區段 -
使用
moduleFileExtensions
宣告新增jest
頂層金鑰
這些變更會顯示在下列大綱中。將新的文字放在 中指示的位置package.json
。「...」預留位置表示檔案的現有部分不應變更。
{
...
"scripts": {
...
"test": "jest"
},
"devDependencies": {
...
"@types/jest": "^24.0.18",
"jest": "^24.9.0"
},
"jest": {
"moduleFileExtensions": ["js"]
}
}
堆疊範例
以下是本主題中將測試的堆疊。如先前所述,其中包含 Lambda 函數和 Step Functions 狀態機器,並接受一或多個 Amazon SNS 主題。Lambda 函數已訂閱 Amazon SNS 主題,並將其轉送至狀態機器。
您不需要執行任何特殊動作,即可測試應用程式。事實上,此 CDK 堆疊與本指南中的其他範例堆疊沒有任何重要差異。
在 中放置下列程式碼lib/state-machine-stack.ts
:
import * as cdk from "aws-cdk-lib";
import * as sns from "aws-cdk-lib/aws-sns";
import * as sns_subscriptions from "aws-cdk-lib/aws-sns-subscriptions";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as sfn from "aws-cdk-lib/aws-stepfunctions";
import { Construct } from "constructs";
export interface StateMachineStackProps extends cdk.StackProps {
readonly topics: sns.Topic[];
}
export class StateMachineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: StateMachineStackProps) {
super(scope, id, props);
// In the future this state machine will do some work...
const stateMachine = new sfn.StateMachine(this, "StateMachine", {
definition: new sfn.Pass(this, "StartState"),
});
// This Lambda function starts the state machine.
const func = new lambda.Function(this, "LambdaFunction", {
runtime: lambda.Runtime.NODEJS_18_X,
handler: "handler",
code: lambda.Code.fromAsset("./start-state-machine"),
environment: {
STATE_MACHINE_ARN: stateMachine.stateMachineArn,
},
});
stateMachine.grantStartExecution(func);
const subscription = new sns_subscriptions.LambdaSubscription(func);
for (const topic of props.topics) {
topic.addSubscription(subscription);
}
}
}
我們將修改應用程式的主要進入點,以便我們不會實際執行個體化堆疊。我們不想意外部署它。我們的測試會建立應用程式和堆疊執行個體以供測試。與測試驅動的開發結合時,這是一個有用的策略:在啟用部署之前,請確保堆疊通過所有測試。
在 bin/state-machine.ts
中:
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib";
const app = new cdk.App();
// Stacks are intentionally not created here -- this application isn't meant to
// be deployed.
Lambda 函數
我們的範例堆疊包含啟動狀態機器的 Lambda 函數。我們必須提供此函數的原始程式碼,讓 CDK 可以綁定和部署它,做為建立 Lambda 函數資源的一部分。
-
在
start-state-machine
應用程式的主目錄中建立 資料夾。 -
在此資料夾中,建立至少一個檔案。例如,您可以在 中儲存下列程式碼
start-state-machines/index.js
。exports.handler = async function (event, context) { return 'hello world'; };
不過,任何檔案都可以運作,因為我們實際上不會部署堆疊。
執行測試
以下是您在 AWS CDK 應用程式中用來執行測試的命令,供您參考。這些命令與您用來在任何使用相同測試架構的專案中執行測試的命令相同。對於需要建置步驟的語言,請包含 ,以確保您的測試已編譯。
$
tsc && npm test
精細聲明
使用精細聲明測試堆疊的第一個步驟是合成堆疊,因為我們要針對產生的 AWS CloudFormation 範本撰寫聲明。
我們的 StateMachineStackStack
要求我們將 Amazon SNS 主題傳遞給它,以轉送至狀態機器。因此,在我們的測試中,我們將建立單獨的堆疊來包含主題。
通常,撰寫 CDK 應用程式時,您可以在堆疊的建構函式中,將 Amazon SNS 主題進行子類別Stack
和執行個體化。在我們的測試中,我們會Stack
直接執行個體化,然後將此堆疊作為 Topic
的範圍傳遞,將其連接到堆疊。這在功能上相當且較不詳細。它還有助於讓堆疊僅用於與您打算部署的堆疊「看起來不同」的測試。
import { Capture, Match, Template } from "aws-cdk-lib/assertions";
import * as cdk from "aws-cdk-lib";
import * as sns from "aws-cdk-lib/aws-sns";
import { StateMachineStack } from "../lib/state-machine-stack";
describe("StateMachineStack", () => {
test("synthesizes the way we expect", () => {
const app = new cdk.App();
// Since the StateMachineStack consumes resources from a separate stack
// (cross-stack references), we create a stack for our SNS topics to live
// in here. These topics can then be passed to the StateMachineStack later,
// creating a cross-stack reference.
const topicsStack = new cdk.Stack(app, "TopicsStack");
// Create the topic the stack we're testing will reference.
const topics = [new sns.Topic(topicsStack, "Topic1", {})];
// Create the StateMachineStack.
const stateMachineStack = new StateMachineStack(app, "StateMachineStack", {
topics: topics, // Cross-stack reference
});
// Prepare the stack for assertions.
const template = Template.fromStack(stateMachineStack);
}
現在我們可以宣告 Lambda 函數和 Amazon SNS 訂閱已建立。
// Assert it creates the function with the correct properties...
template.hasResourceProperties("AWS::Lambda::Function", {
Handler: "handler",
Runtime: "nodejs14.x",
});
// Creates the subscription...
template.resourceCountIs("AWS::SNS::Subscription", 1);
我們的 Lambda 函數測試聲明函數資源的兩個特定屬性具有特定值。根據預設, hasResourceProperties
方法會針對合成 CloudFormation 範本中指定的資源屬性執行部分比對。此測試需要提供的屬性存在並具有指定的值,但資源也可以具有其他屬性,而這些屬性未測試。
我們的 Amazon SNS 聲明會宣告合成範本包含訂閱,但不包含訂閱本身。我們主要包含此聲明,以說明如何對資源計數進行聲明。Template
類別提供更具體的方法,以針對 CloudFormation 範本的 Resources
、 Outputs
和 Mapping
區段撰寫聲明。
相符項目
的預設部分比對行為hasResourceProperties
可以使用 Match
類別中的配對器進行變更。
相符項目的範圍從寬鬆 (Match.anyValue
) 到嚴格 (Match.objectEquals
)。它們可以巢狀化,將不同的比對方法套用至資源屬性的不同部分。例如,使用 Match.objectEquals
和 Match.anyValue
一起,我們可以更完整地測試狀態機器的 IAM 角色,同時不需要屬性可能變更的特定值。
// Fully assert on the state machine's IAM role with matchers.
template.hasResourceProperties(
"AWS::IAM::Role",
Match.objectEquals({
AssumeRolePolicyDocument: {
Version: "2012-10-17",
Statement: [
{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: {
Service: {
"Fn::Join": [
"",
["states.", Match.anyValue(), ".amazonaws.com"],
],
},
},
},
],
},
})
);
許多 CloudFormation 資源包含以字串表示的序列化 JSON 物件。Match.serializedJson()
配對器可用來比對此 JSON 中的屬性。
例如,Step Functions 狀態機器是使用以 JSON 為基礎的 Amazon States 語言中的字串來定義。我們會使用 Match.serializedJson()
來確保初始狀態是唯一的步驟。同樣地,我們將使用巢狀比對程式,將不同類型的比對套用至物件的不同部分。
// Assert on the state machine's definition with the Match.serializedJson()
// matcher.
template.hasResourceProperties("AWS::StepFunctions::StateMachine", {
DefinitionString: Match.serializedJson(
// Match.objectEquals() is used implicitly, but we use it explicitly
// here for extra clarity.
Match.objectEquals({
StartAt: "StartState",
States: {
StartState: {
Type: "Pass",
End: true,
// Make sure this state doesn't provide a next state -- we can't
// provide both Next and set End to true.
Next: Match.absent(),
},
},
})
),
});
擷取
測試屬性通常很實用,以確保它們遵循特定格式,或具有與另一個屬性相同的值,而無需事先知道它們的確切值。assertions
模組在其Capture
類別中提供此功能。
透過指定Capture
執行個體來取代 中的值hasResourceProperties
,該值會保留在 Capture
物件中。實際擷取的值可以使用物件as
的方法擷取,包括 asNumber()
、 asString()
和 asObject
,並接受測試。使用 Capture
搭配配對器,指定要在資源屬性中擷取之值的確切位置,包括序列化 JSON 屬性。
下列範例會測試 ,以確保狀態機器的啟動狀態具有以 開頭的名稱Start
。它也會測試此狀態是否存在於機器的狀態清單中。
// Capture some data from the state machine's definition.
const startAtCapture = new Capture();
const statesCapture = new Capture();
template.hasResourceProperties("AWS::StepFunctions::StateMachine", {
DefinitionString: Match.serializedJson(
Match.objectLike({
StartAt: startAtCapture,
States: statesCapture,
})
),
});
// Assert that the start state starts with "Start".
expect(startAtCapture.asString()).toEqual(expect.stringMatching(/^Start/));
// Assert that the start state actually exists in the states object of the
// state machine definition.
expect(statesCapture.asObject()).toHaveProperty(startAtCapture.asString());
快照測試
在快照測試中,您會比較整個合成 CloudFormation 範本與先前儲存的基準 (通常稱為「主要」) 範本。與精細的聲明不同,快照測試在擷取迴歸時沒有用。這是因為快照測試適用於整個範本,而且除了程式碼變更之外,其他事項可能會導致合成結果出現小 (或not-so-small的差異。這些變更甚至不會影響您的部署,但仍會導致快照測試失敗。
例如,您可以更新 CDK 建構以納入新的最佳實務,這可能會導致合成資源或組織方式的變更。或者,您可以將 CDK Toolkit 更新為報告其他中繼資料的版本。內容值的變更也會影響合成的範本。
不過,只要您持續保留可能影響合成範本的所有其他因素,快照測試在重構方面很有幫助。如果您所做的變更意外變更範本,您會立即知道。如果變更是刻意的,只要接受新範本做為基準即可。
例如,如果我們擁有此DeadLetterQueue
建構:
export class DeadLetterQueue extends sqs.Queue {
public readonly messagesInQueueAlarm: cloudwatch.IAlarm;
constructor(scope: Construct, id: string) {
super(scope, id);
// Add the alarm
this.messagesInQueueAlarm = new cloudwatch.Alarm(this, 'Alarm', {
alarmDescription: 'There are messages in the Dead Letter Queue',
evaluationPeriods: 1,
threshold: 1,
metric: this.metricApproximateNumberOfMessagesVisible(),
});
}
}
我們可以用下列方式進行測試:
import { Match, Template } from "aws-cdk-lib/assertions";
import * as cdk from "aws-cdk-lib";
import { DeadLetterQueue } from "../lib/dead-letter-queue";
describe("DeadLetterQueue", () => {
test("matches the snapshot", () => {
const stack = new cdk.Stack();
new DeadLetterQueue(stack, "DeadLetterQueue");
const template = Template.fromStack(stack);
expect(template.toJSON()).toMatchSnapshot();
});
});
測試提示
請記住,只要測試的程式碼,您的測試就會持續運作,而且會像往常一樣讀取和修改。因此,需要一些時間來考慮如何最好地撰寫它們。
請勿複製和貼上設定行或常見聲明。反之,請將此邏輯重構為固定裝置或協助程式函數。使用良好名稱來反映每個測試實際測試的內容。
請勿嘗試在一次測試中執行太多操作。最好是,測試應該測試一種行為,而且只測試一種行為。如果您不小心中斷了該行為,則只會有一個測試失敗,而且測試的名稱應該告訴您失敗了哪些。不過,這更適合努力;有時您會無法避免 (或無意中) 撰寫測試,以測試多個行為。快照測試是基於我們之前已說明的原因,特別是容易發生此問題,因此請謹慎使用。