這是 AWS CDK v2 開發人員指南。較舊的 CDK v1 已於 2022 年 6 月 1 日進入維護,並於 2023 年 6 月 1 日結束支援。
本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。
有了 AWS CDK,您的基礎結構可以像您撰寫的任何其他程式碼一樣進行測試。測試 AWS CDK 應用程序的標準方法使用 AWS CDK的斷言模塊和流行的測試框架,如 Jest
您可以為 AWS CDK 應用程序編寫兩類測試。
-
細粒度斷言會測試生成的 AWS CloudFormation 模板的特定方面,例如「此資源具有此值的此屬性」。這些測試可以檢測回歸。當您使用測試驅動開發開發來開發新功能時,它們也很有用。(您可以先編寫測試,然後通過編寫正確的實現來通過它。) 細粒度斷言是最常用的測試。
-
快照測試會根據先前儲存的基準 AWS CloudFormation 範本來測試合成的範本。快照測試使您可以自由重構,因為您可以確保重構代碼的工作方式與原始代碼完全相同。如果變更是有意為之,您可以接受未來測試的新基準。不過,CDK升級也可能導致合成範本發生變更,因此您不能只仰賴快照來確保您的實作正確無誤。
注意
本主題中作為範例使用的 TypeScript、Python 和 Java 應用程式的完整版本可在上
開始使用
為了說明如何編寫這些測試,我們將創建一個包含 AWS Step Functions 狀態機和 AWS Lambda 函數的堆棧。Lambda 函數已訂閱 Amazon SNS 主題,只需將消息轉發到狀態機即可。
首先,使用 CDK Toolkit 創建一個空的CDK應用程序項目,並安裝我們需要的庫。我們將使用的構造都在 main 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 要收集哪些類型的文件。必要的變更如下。
-
將新的
test
金鑰新增至scripts
區段 -
將開玩笑及其類型添加到該
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 州語言的字串定義的。我們將用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();
});
});
測試提示
請記住,您的測試將只要他們測試的代碼一樣長,並且它們將一樣經常被讀取和修改。因此,花點時間考慮如何最好地編寫它們是值得的。
不要複製和粘貼設置行或常見斷言。相反,將此邏輯重構為夾具或輔助函數。使用反映每個測試實際測試的好名稱。
不要試圖在一次測試中做太多。最好是,測試應該測試一個且只能測試一種行為。如果您不小心破壞了該行為,則應該只有一個測試失敗,並且測試的名稱應該告訴您失敗的內容。然而,這是一個更理想的努力; 有時你會不可避免地(或無意中)編寫測試多個行為的測試。由於我們已經描述的原因,快照測試尤其容易出現此問題,因此請謹慎使用它們。