測試結構 - AWS Cloud Development Kit (AWS CDK) V2

這是 AWS CDK v2 開發人員指南。較舊的 CDK 第 1 版已於 2022 年 6 月 1 日進入維護,並於 2023 年 6 月 1 日結束支援。

本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。

測試結構

有了 AWS CDK,您的基礎結構可以像您撰寫的任何其他程式碼一樣進行測試。測試 AWS CDK 應用程序的標準方法使用 AWS CDK的斷言模塊和流行的測試框架,如 Jest for TypeScript 和 JavaScript 或 Pytest for Python。

您可以為 AWS CDK 應用程序編寫兩類測試。

  • 細粒度斷言會測試生成的 AWS CloudFormation 模板的特定方面,例如「此資源具有此值的此屬性」。這些測試可以檢測回歸。當您使用測試驅動開發開發來開發新功能時,它們也很有用。(您可以先編寫測試,然後通過編寫正確的實現來通過它。) 細粒度斷言是最常用的測試。

  • 快照測試會根據先前儲存的基準 AWS CloudFormation 範本來測試合成的範本。快照測試使您可以自由重構,因為您可以確保重構代碼的工作方式與原始代碼完全相同。如果變更是有意為之,您可以接受未來測試的新基準。不過,CDK 升級也可能導致合成範本發生變更,因此您不能只依賴快照來確保您的實作正確無誤。

注意

本主題中作為範例使用的 TypeScript、Python 和 Java 應用程式的完整版本可在上取得 GitHub。

開始使用

為了說明如何編寫這些測試,我們將創建一個包含 AWS Step Functions 狀態機和 AWS Lambda 函數的堆棧。Lambda 函數已訂閱 Amazon SNS 主題,只需將訊息轉寄到狀態機器即可。

首先,使用 CDK Toolkit 創建一個空的 CDK 應用程序項目並安裝我們需要的庫。我們將使用的構造都在主 CDK 包中,這是使用 CDK Toolkit 創建的項目中的默認依賴項。但是,您必須安裝測試框架。

TypeScript
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區段

  • 將開玩笑及其類型添加到該devDependencies部分

  • 使用moduleFileExtensions聲明添加新的jest頂級密鑰

這些變更顯示在下列大綱中。將新文字置於中所示的位置package.json。「...」預留位置表示檔案中不應變更的現有部分。

{ ... "scripts": { ... "test": "jest" }, "devDependencies": { ... "@types/jest": "^24.0.18", "jest": "^24.9.0" }, "jest": { "moduleFileExtensions": ["js"] } }
JavaScript
mkdir state-machine && cd state-machine cdk init --language=javascript npm install --save-dev jest

為您的測試創建一個目錄。

mkdir test

編輯該項目package.json以告訴 NPM 如何運行 Jest,並告訴 Jest 要收集哪些類型的文件。必要的變更如下。

  • 將新的test金鑰新增至scripts區段

  • 將開玩笑添加到該部分 devDependencies

  • 使用moduleFileExtensions聲明添加新的jest頂級密鑰

這些變更顯示在下列大綱中。將新文字置於中所示的位置package.json。「...」佔位符表示文件中不應該更改的現有部分。

{ ... "scripts": { ... "test": "jest" }, "devDependencies": { ... "jest": "^24.9.0" }, "jest": { "moduleFileExtensions": ["js"] } }
Python
mkdir state-machine && cd state-machine cdk init --language=python source .venv/bin/activate # On Windows, run '.\venv\Scripts\activate' instead python -m pip install -r requirements.txt python -m pip install -r requirements-dev.txt
Java
mkdir state-machine && cd-state-machine cdk init --language=java

在您偏好的 Java IDE 中開啟專案。(在 Eclipse 中,使用文件 > 導入 > 現有的 Maven 項目。)

C#
mkdir state-machine && cd-state-machine cdk init --language=csharp

src\StateMachine.sln在視覺工作室中開啟。

以滑鼠右鍵按一下 [方案總管] 中的方案,然後選擇 [新] 搜索 MSTest C# 並為 C# 添加一個 MSTest 測試項目。(預設名稱TestProject1沒問題。)

按一下滑鼠右鍵TestProject1並選擇「新增」>「專案參考」,然後將StateMachine專案新增為參考。

示例堆棧

以下是將在本主題中測試的堆疊。如前所述,它包含 Lambda 函數和 Step Functions 數狀態機器,並接受一或多個 Amazon SNS 主題。Lambda 函數已訂閱 Amazon SNS 主題,並將其轉寄至狀態機器。

您不必做任何特別的事情即可使應用程序可測試。實際上,此 CDK 堆棧與本指南中的其他示例堆棧在任何重要的方式上都沒有區別。

TypeScript

將下列程式碼放在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); } } }
JavaScript

將下列程式碼放在lib/state-machine-stack.js

const cdk = require("aws-cdk-lib"); const sns = require("aws-cdk-lib/aws-sns"); const sns_subscriptions = require("aws-cdk-lib/aws-sns-subscriptions"); const lambda = require("aws-cdk-lib/aws-lambda"); const sfn = require("aws-cdk-lib/aws-stepfunctions"); class StateMachineStack extends cdk.Stack { constructor(scope, id, props) { 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); } } } module.exports = { StateMachineStack }
Python

將下列程式碼放在state_machine/state_machine_stack.py

from typing import List import aws_cdk.aws_lambda as lambda_ import aws_cdk.aws_sns as sns import aws_cdk.aws_sns_subscriptions as sns_subscriptions import aws_cdk.aws_stepfunctions as sfn import aws_cdk as cdk class StateMachineStack(cdk.Stack): def __init__( self, scope: cdk.Construct, construct_id: str, *, topics: List[sns.Topic], **kwargs ) -> None: super().__init__(scope, construct_id, **kwargs) # In the future this state machine will do some work... state_machine = sfn.StateMachine( self, "StateMachine", definition=sfn.Pass(self, "StartState") ) # This Lambda function starts the state machine. func = lambda_.Function( self, "LambdaFunction", runtime=lambda_.Runtime.NODEJS_18_X, handler="handler", code=lambda_.Code.from_asset("./start-state-machine"), environment={ "STATE_MACHINE_ARN": state_machine.state_machine_arn, }, ) state_machine.grant_start_execution(func) subscription = sns_subscriptions.LambdaSubscription(func) for topic in topics: topic.add_subscription(subscription)
Java
package software.amazon.samples.awscdkassertionssamples; import software.constructs.Construct; import software.amazon.awscdk.Stack; import software.amazon.awscdk.StackProps; import software.amazon.awscdk.services.lambda.Code; import software.amazon.awscdk.services.lambda.Function; import software.amazon.awscdk.services.lambda.Runtime; import software.amazon.awscdk.services.sns.ITopicSubscription; import software.amazon.awscdk.services.sns.Topic; import software.amazon.awscdk.services.sns.subscriptions.LambdaSubscription; import software.amazon.awscdk.services.stepfunctions.Pass; import software.amazon.awscdk.services.stepfunctions.StateMachine; import java.util.Collections; import java.util.List; public class StateMachineStack extends Stack { public StateMachineStack(final Construct scope, final String id, final List<Topic> topics) { this(scope, id, null, topics); } public StateMachineStack(final Construct scope, final String id, final StackProps props, final List<Topic> topics) { super(scope, id, props); // In the future this state machine will do some work... final StateMachine stateMachine = StateMachine.Builder.create(this, "StateMachine") .definition(new Pass(this, "StartState")) .build(); // This Lambda function starts the state machine. final Function func = Function.Builder.create(this, "LambdaFunction") .runtime(Runtime.NODEJS_18_X) .handler("handler") .code(Code.fromAsset("./start-state-machine")) .environment(Collections.singletonMap("STATE_MACHINE_ARN", stateMachine.getStateMachineArn())) .build(); stateMachine.grantStartExecution(func); final ITopicSubscription subscription = new LambdaSubscription(func); for (final Topic topic : topics) { topic.addSubscription(subscription); } } }
C#
using Amazon.CDK; using Amazon.CDK.AWS.Lambda; using Amazon.CDK.AWS.StepFunctions; using Amazon.CDK.AWS.SNS; using Amazon.CDK.AWS.SNS.Subscriptions; using Constructs; using System.Collections.Generic; namespace AwsCdkAssertionSamples { public class StateMachineStackProps : StackProps { public Topic[] Topics; } public class StateMachineStack : Stack { internal StateMachineStack(Construct scope, string id, StateMachineStackProps props = null) : base(scope, id, props) { // In the future this state machine will do some work... var stateMachine = new StateMachine(this, "StateMachine", new StateMachineProps { Definition = new Pass(this, "StartState") }); // This Lambda function starts the state machine. var func = new Function(this, "LambdaFunction", new FunctionProps { Runtime = Runtime.NODEJS_18_X, Handler = "handler", Code = Code.FromAsset("./start-state-machine"), Environment = new Dictionary<string, string> { { "STATE_MACHINE_ARN", stateMachine.StateMachineArn } } }); stateMachine.GrantStartExecution(func); foreach (Topic topic in props?.Topics ?? new Topic[0]) { var subscription = new LambdaSubscription(func); } } } }

我們將修改應用程序的主入口點,以便我們不實際實例化我們的堆棧。我們不想不小心部署它。我們的測試將創建一個應用程序和一個用於測試的堆棧實例。與測試驅動開發結合使用時,這是一種有用的策略:在啟用部署之前確保堆棧通過所有測試。

TypeScript

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.
JavaScript

bin/state-machine.js 中:

#!/usr/bin/env node const cdk = require("aws-cdk-lib"); const app = new cdk.App(); // Stacks are intentionally not created here -- this application isn't meant to // be deployed.
Python

app.py 中:

#!/usr/bin/env python3 import os import aws_cdk as cdk app = cdk.App() # Stacks are intentionally not created here -- this application isn't meant to # be deployed. app.synth()
Java
package software.amazon.samples.awscdkassertionssamples; import software.amazon.awscdk.App; public class SampleApp { public static void main(final String[] args) { App app = new App(); // Stacks are intentionally not created here -- this application isn't meant to be deployed. app.synth(); } }
C#
using Amazon.CDK; namespace AwsCdkAssertionSamples { sealed class Program { public static void Main(string[] args) { var app = new App(); // Stacks are intentionally not created here -- this application isn't meant to be deployed. app.Synth(); } } }

Lambda 函數

我們的範例堆疊包含啟動狀態機器的 Lambda 函數。我們必須提供此函數的原始程式碼,以便 CDK 可以將其捆綁並部署為建立 Lambda 函數資源的一部分。

  • start-state-machine在應用程序的主目錄中創建文件夾。

  • 在此資料夾中,至少建立一個檔案。例如,您可以在中儲存下列程式碼start-state-machines/index.js

    exports.handler = async function (event, context) { return 'hello world'; };

    但是,任何文件都可以工作,因為我們實際上不會部署堆棧。

執行測試

作為參考,以下是您用於在應用 AWS CDK 程序中運行測試的命令。這些命令與您在使用相同測試框架的任何項目中用於運行測試的命令相同。對於需要構建步驟的語言,請包括以確保您的測試已編譯。

TypeScript
tsc && npm test
JavaScript
npm test
Python
python -m pytest
Java
mvn compile && mvn test
C#

構建您的解決方案(F6)以發現測試,然後運行測試(測試 > 運行所有測試)。要選擇要運行的測試,請打開測試資源管理器(測試 > 測試資源管理器)。

或者:

dotnet test src

細粒度斷言

使用細粒度斷言測試堆棧的第一步是合成堆棧,因為我們正在針對生成的模板編寫斷言。 AWS CloudFormation

我們StateMachineStackStack要求我們將其傳遞給 Amazon SNS 主題,以轉發到狀態機。因此,在我們的測試中,我們將創建一個單獨的堆棧來包含該主題。

通常,在編寫 CDK 應用程序時,您可以在堆棧的構造函數中對 Amazon SNS 主題進行子類別Stack和實例化。在我們的測試中,我們Stack直接實例化,然後將此堆棧作為範圍傳遞,將其附加到堆棧中。Topic這在功能上是等價的,而且不太冗長。它還有助於使僅在測試中使用的堆棧與您打算部署的堆棧「看起來不同」。

TypeScript
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); }
JavaScript
const { Capture, Match, Template } = require("aws-cdk-lib/assertions"); const cdk = require("aws-cdk-lib"); const sns = require("aws-cdk-lib/aws-sns"); const { StateMachineStack } = require("../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);
Python
from aws_cdk import aws_sns as sns import aws_cdk as cdk from aws_cdk.assertions import Template from app.state_machine_stack import StateMachineStack def test_synthesizes_properly(): app = 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. topics_stack = cdk.Stack(app, "TopicsStack") # Create the topic the stack we're testing will reference. topics = [sns.Topic(topics_stack, "Topic1")] # Create the StateMachineStack. state_machine_stack = StateMachineStack( app, "StateMachineStack", topics=topics # Cross-stack reference ) # Prepare the stack for assertions. template = Template.from_stack(state_machine_stack)
Java
package software.amazon.samples.awscdkassertionssamples; import org.junit.jupiter.api.Test; import software.amazon.awscdk.assertions.Capture; import software.amazon.awscdk.assertions.Match; import software.amazon.awscdk.assertions.Template; import software.amazon.awscdk.App; import software.amazon.awscdk.Stack; import software.amazon.awscdk.services.sns.Topic; import java.util.*; import static org.assertj.core.api.Assertions.assertThat; public class StateMachineStackTest { @Test public void testSynthesizesProperly() { final App app = new 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. final Stack topicsStack = new Stack(app, "TopicsStack"); // Create the topic the stack we're testing will reference. final List<Topic> topics = Collections.singletonList(Topic.Builder.create(topicsStack, "Topic1").build()); // Create the StateMachineStack. final StateMachineStack stateMachineStack = new StateMachineStack( app, "StateMachineStack", topics // Cross-stack reference ); // Prepare the stack for assertions. final Template template = Template.fromStack(stateMachineStack)
C#
using Microsoft.VisualStudio.TestTools.UnitTesting; using Amazon.CDK; using Amazon.CDK.AWS.SNS; using Amazon.CDK.Assertions; using AwsCdkAssertionSamples; using ObjectDict = System.Collections.Generic.Dictionary<string, object>; using StringDict = System.Collections.Generic.Dictionary<string, string>; namespace TestProject1 { [TestClass] public class StateMachineStackTest { [TestMethod] public void TestMethod1() { var app = new 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. var topicsStack = new Stack(app, "TopicsStack"); // Create the topic the stack we're testing will reference. var topics = new Topic[] { new Topic(topicsStack, "Topic1") }; // Create the StateMachineStack. var StateMachineStack = new StateMachineStack(app, "StateMachineStack", new StateMachineStackProps { Topics = topics }); // Prepare the stack for assertions. var template = Template.FromStack(stateMachineStack); // test will go here } } }

現在,我們可以斷言 Lambda 函數和 Amazon SNS 訂閱已經建立。

TypeScript
// 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);
JavaScript
// 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);
Python
# Assert that we have created the function with the correct properties template.has_resource_properties( "AWS::Lambda::Function", { "Handler": "handler", "Runtime": "nodejs14.x", }, ) # Assert that we have created a subscription template.resource_count_is("AWS::SNS::Subscription", 1)
Java
// Assert it creates the function with the correct properties... template.hasResourceProperties("AWS::Lambda::Function", Map.of( "Handler", "handler", "Runtime", "nodejs14.x" )); // Creates the subscription... template.resourceCountIs("AWS::SNS::Subscription", 1);
C#
// Prepare the stack for assertions. var template = Template.FromStack(stateMachineStack); // Assert it creates the function with the correct properties... template.HasResourceProperties("AWS::Lambda::Function", new StringDict { { "Handler", "handler"}, { "Runtime", "nodejs14x" } }); // Creates the subscription... template.ResourceCountIs("AWS::SNS::Subscription", 1);

我們的 Lambda 函數測試聲明函數資源的兩個特定屬性具有特定的值。默認情況下,該hasResourceProperties方法對合成 CloudFormation 模板中給出的資源屬性執行部分匹配。此測試要求提供的屬性存在並具有指定的值,但資源也可以具有未測試的其他屬性。

我們的 Amazon SNS 聲明聲稱合成範本包含訂閱,但沒有與訂閱本身有關的內容。我們包括這個斷言主要是為了說明如何斷言資源計數。此Template類別提供更具體的方法,可針對 CloudFormation 範本的ResourcesOutputs、和Mapping區段撰寫宣告。

匹配器

的預設部分相符行為hasResourceProperties可以使用Match類別中的匹配器來變更。

匹配器範圍從寬鬆(Match.anyValue)到嚴格(Match.objectEquals)。它們可以嵌套,以將不同的匹配方法應用於資源屬性的不同部分。例如,使用Match.objectEqualsMatch.anyValue一起使用,我們可以更完整地測試狀態機器的 IAM 角色,而不需要可能變更的屬性的特定值。

TypeScript
// 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"], ], }, }, }, ], }, }) );
JavaScript
// 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"], ], }, }, }, ], }, }) );
Python
from aws_cdk.assertions import Match # Fully assert on the state machine's IAM role with matchers. template.has_resource_properties( "AWS::IAM::Role", Match.object_equals( { "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": { "Fn::Join": [ "", [ "states.", Match.any_value(), ".amazonaws.com", ], ], }, }, }, ], }, } ), )
Java
// Fully assert on the state machine's IAM role with matchers. template.hasResourceProperties("AWS::IAM::Role", Match.objectEquals( Collections.singletonMap("AssumeRolePolicyDocument", Map.of( "Version", "2012-10-17", "Statement", Collections.singletonList(Map.of( "Action", "sts:AssumeRole", "Effect", "Allow", "Principal", Collections.singletonMap( "Service", Collections.singletonMap( "Fn::Join", Arrays.asList( "", Arrays.asList("states.", Match.anyValue(), ".amazonaws.com") ) ) ) )) )) ));
C#
// Fully assert on the state machine's IAM role with matchers. template.HasResource("AWS::IAM::Role", Match.ObjectEquals(new ObjectDict { { "AssumeRolePolicyDocument", new ObjectDict { { "Version", "2012-10-17" }, { "Action", "sts:AssumeRole" }, { "Principal", new ObjectDict { { "Version", "2012-10-17" }, { "Statement", new object[] { new ObjectDict { { "Action", "sts:AssumeRole" }, { "Effect", "Allow" }, { "Principal", new ObjectDict { { "Service", new ObjectDict { { "", new object[] { "states", Match.AnyValue(), ".amazonaws.com" } } } } } } } } } } } } } }));

許多 CloudFormation 資源包括以字符串表示的序列化 JSON 對象。Match.serializedJson()匹配器可用於匹配此 JSON 內的屬性。

例如,Step Functions 狀態機是使用以 JSON 為基礎的 Amazon 州語言中的字串定義的。我們將用Match.serializedJson()來確保我們的初始狀態是唯一的步驟。同樣,我們將使用嵌套匹配器將不同種類的匹配應用於對象的不同部分。

TypeScript
// 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(), }, }, }) ), });
JavaScript
// 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(), }, }, }) ), });
Python
# Assert on the state machine's definition with the serialized_json matcher. template.has_resource_properties( "AWS::StepFunctions::StateMachine", { "DefinitionString": Match.serialized_json( # Match.object_equals() is the default, but specify it here for clarity Match.object_equals( { "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(), }, }, } ) ), }, )
Java
// Assert on the state machine's definition with the Match.serializedJson() matcher. template.hasResourceProperties("AWS::StepFunctions::StateMachine", Collections.singletonMap( "DefinitionString", Match.serializedJson( // Match.objectEquals() is used implicitly, but we use it explicitly here for extra clarity. Match.objectEquals(Map.of( "StartAt", "StartState", "States", Collections.singletonMap( "StartState", Map.of( "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() ) ) )) ) ));
C#
// Assert on the state machine's definition with the Match.serializedJson() matcher template.HasResourceProperties("AWS::StepFunctions::StateMachine", new ObjectDict { { "DefinitionString", Match.SerializedJson( // Match.objectEquals() is used implicitly, but we use it explicitly here for extra clarity. Match.ObjectEquals(new ObjectDict { { "StartAt", "StartState" }, { "States", new ObjectDict { { "StartState", new ObjectDict { { "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。它還測試此狀態是否存在於機器中的狀態列表中。

TypeScript
// 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());
JavaScript
// 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());
Python
import re from aws_cdk.assertions import Capture # ... # Capture some data from the state machine's definition. start_at_capture = Capture() states_capture = Capture() template.has_resource_properties( "AWS::StepFunctions::StateMachine", { "DefinitionString": Match.serialized_json( Match.object_like( { "StartAt": start_at_capture, "States": states_capture, } ) ), }, ) # Assert that the start state starts with "Start". assert re.match("^Start", start_at_capture.as_string()) # Assert that the start state actually exists in the states object of the # state machine definition. assert start_at_capture.as_string() in states_capture.as_object()
Java
// Capture some data from the state machine's definition. final Capture startAtCapture = new Capture(); final Capture statesCapture = new Capture(); template.hasResourceProperties("AWS::StepFunctions::StateMachine", Collections.singletonMap( "DefinitionString", Match.serializedJson( Match.objectLike(Map.of( "StartAt", startAtCapture, "States", statesCapture )) ) )); // Assert that the start state starts with "Start". assertThat(startAtCapture.asString()).matches("^Start.+"); // Assert that the start state actually exists in the states object of the state machine definition. assertThat(statesCapture.asObject()).containsKey(startAtCapture.asString());
C#
// Capture some data from the state machine's definition. var startAtCapture = new Capture(); var statesCapture = new Capture(); template.HasResourceProperties("AWS::StepFunctions::StateMachine", new ObjectDict { { "DefinitionString", Match.SerializedJson( new ObjectDict { { "StartAt", startAtCapture }, { "States", statesCapture } } )} }); Assert.IsTrue(startAtCapture.ToString().StartsWith("Start")); Assert.IsTrue(statesCapture.AsObject().ContainsKey(startAtCapture.ToString()));

快照測試

快照測試中,您可以將整個合成 CloudFormation 範本與先前儲存的基準 (通常稱為「主要」) 範本進行比較。與細粒度斷言不同,快照測試在捕獲回歸方面沒有用。這是因為快照集測試適用於整個範本,除了程式碼變更之外,其他項目可能會導致合成結果的小 (或 not-so-small) 差異。這些變更甚至可能不會影響您的部署,但仍會導致快照測試失敗。

例如,您可以更新 CDK 建構以納入新的最佳實務,這可能會導致合成資源的變更或組織方式。或者,您可以將 CDK Toolkit 更新為報告其他中繼資料的版本。對前後關聯值的變更也會影響合成範本。

但是,只要您保持不變,可能會影響合成模板的所有其他因素,快照測試在重構方面有很大的幫助。如果您所做的變更意外變更了範本,您將立即知道。如果是故意的變更,只要接受新範本作為基準線即可。

例如,如果我們有這樣的DeadLetterQueue結構:

TypeScript
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(), }); } }
JavaScript
class DeadLetterQueue extends sqs.Queue { constructor(scope, id) { 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(), }); } } module.exports = { DeadLetterQueue }
Python
class DeadLetterQueue(sqs.Queue): def __init__(self, scope: Construct, id: str): super().__init__(scope, id) self.messages_in_queue_alarm = cloudwatch.Alarm( self, "Alarm", alarm_description="There are messages in the Dead Letter Queue.", evaluation_periods=1, threshold=1, metric=self.metric_approximate_number_of_messages_visible(), )
Java
public class DeadLetterQueue extends Queue { private final IAlarm messagesInQueueAlarm; public DeadLetterQueue(@NotNull Construct scope, @NotNull String id) { super(scope, id); this.messagesInQueueAlarm = Alarm.Builder.create(this, "Alarm") .alarmDescription("There are messages in the Dead Letter Queue.") .evaluationPeriods(1) .threshold(1) .metric(this.metricApproximateNumberOfMessagesVisible()) .build(); } public IAlarm getMessagesInQueueAlarm() { return messagesInQueueAlarm; } }
C#
namespace AwsCdkAssertionSamples { public class DeadLetterQueue : Queue { public IAlarm messagesInQueueAlarm; public DeadLetterQueue(Construct scope, string id) : base(scope, id) { messagesInQueueAlarm = new Alarm(this, "Alarm", new AlarmProps { AlarmDescription = "There are messages in the Dead Letter Queue.", EvaluationPeriods = 1, Threshold = 1, Metric = this.MetricApproximateNumberOfMessagesVisible() }); } } }

我們可以像這樣測試它:

TypeScript
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(); }); });
JavaScript
const { Match, Template } = require("aws-cdk-lib/assertions"); const cdk = require("aws-cdk-lib"); const { DeadLetterQueue } = require("../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(); }); });
Python
import aws_cdk_lib as cdk from aws_cdk_lib.assertions import Match, Template from app.dead_letter_queue import DeadLetterQueue def snapshot_test(): stack = cdk.Stack() DeadLetterQueue(stack, "DeadLetterQueue") template = Template.from_stack(stack) assert template.to_json() == snapshot
Java
package software.amazon.samples.awscdkassertionssamples; import org.junit.jupiter.api.Test; import au.com.origin.snapshots.Expect; import software.amazon.awscdk.assertions.Match; import software.amazon.awscdk.assertions.Template; import software.amazon.awscdk.Stack; import java.util.Collections; import java.util.Map; public class DeadLetterQueueTest { @Test public void snapshotTest() { final Stack stack = new Stack(); new DeadLetterQueue(stack, "DeadLetterQueue"); final Template template = Template.fromStack(stack); expect.toMatchSnapshot(template.toJSON()); } }
C#
using Microsoft.VisualStudio.TestTools.UnitTesting; using Amazon.CDK; using Amazon.CDK.Assertions; using AwsCdkAssertionSamples; using ObjectDict = System.Collections.Generic.Dictionary<string, object>; using StringDict = System.Collections.Generic.Dictionary<string, string>; namespace TestProject1 { [TestClass] public class StateMachineStackTest [TestClass] public class DeadLetterQueueTest { [TestMethod] public void SnapshotTest() { var stack = new Stack(); new DeadLetterQueue(stack, "DeadLetterQueue"); var template = Template.FromStack(stack); return Verifier.Verify(template.ToJSON()); } } }

測試提示

請記住,您的測試將只要他們測試的代碼一樣長,並且它們將一樣經常被讀取和修改。因此,花點時間考慮如何最好地編寫它們是值得的。

不要複製和粘貼設置行或常見斷言。相反,將此邏輯重構為夾具或輔助函數。使用反映每個測試實際測試的好名稱。

不要試圖在一次測試中做太多。最好是,測試應該測試一個且只能測試一種行為。如果您不小心破壞了該行為,則應該只有一個測試失敗,並且測試的名稱應該告訴您失敗的內容。然而,這是一個更理想的努力; 有時你會不可避免地(或無意中)編寫測試多個行為的測試。由於我們已經描述的原因,快照測試尤其容易出現此問題,因此請謹慎使用它們。