这是 AWS CDK v2 开发者指南。较旧的 CDK v1 于 2022 年 6 月 1 日进入维护阶段,并于 2023 年 6 月 1 日终止支持。
本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。
测试 AWS CDK 应用程序
使用后 AWS CDK,您的基础架构可以像您编写的任何其他代码一样具有可测试性。测试 AWS CDK 应用程序的标准方法使用断言模块和流行 AWS CDK的测试框架,例如 Jest fo JavaScript r and 和 Python 的 TypeScript Pyt
您可以为 AWS CDK 应用程序编写两类测试。
-
细粒度的断言测试生成 AWS CloudFormation 模板的特定方面,例如 “此资源具有此值的此属性”。这些测试可以检测回归。当你使用测试驱动开发来开发新功能时,它们也很有用。(你可以先写一个测试,然后通过写一个正确的实现来让它通过。) 细粒度断言是最常用的测试。
-
快照测试根据先前存储的基线 AWS CloudFormation 模板测试合成后的模板。Snapshot 测试允许您自由重构,因为您可以确保重构后的代码与原始代码完全相同。如果这些更改是有意的,您可以接受新的基线,用于今后的测试。但是,CDK升级也可能导致合成模板发生变化,因此您不能仅仅依靠快照来确保实现正确。
注意
本主题中用作示例的 TypeScript、Python 和 Java 应用程序的完整版本可在上找到 GitHub
开始使用
为了说明如何编写这些测试,我们将创建一个包含 AWS Step Functions 状态机和 AWS Lambda 函数的堆栈。Lambda 函数订阅了 Amazon SNS 主题,只需将消息转发到状态机即可。
首先,使用 CDK Toolkit 创建一个空的CDK应用程序项目并安装我们需要的库。我们将使用的构造都在主CDK包中,这是使用 T CDK oolkit 创建的项目中的默认依赖项。但是,您必须安装测试框架。
- 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 要收集什么类型的文件。必要的更改如下。-
向该
scripts
部分添加新test
密钥 -
将 Jest 及其类型添加到该部分
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 要收集什么类型的文件。必要的更改如下。-
向该
scripts
部分添加新test
密钥 -
将 Jest 添加到该部分
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
在 Visual Studio 中打开
src\StateMachine.sln
。在 “解决方案资源管理器” 中右键单击该解决方案,然后选择 “添加” > “新建项目”。搜索 C MSTest # 并为 C# 添加MSTest测试项目。(默认名称
TestProject1
没问题。)右键单击
TestProject1
并选择 “添加” > “项目引用”,然后将该StateMachine
项目添加为参考。
示例堆栈
以下是将在本主题中测试的堆栈。如前所述,它包含一个 Lambda 函数和一个 Step Functions 状态机,并接受一个或多个亚马逊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
-
In
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
-
In
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
-
In
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应用程序时,可以在堆栈的构造函数中对 A SNS mazon 主题进行子类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 函数和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 模板的Resources
Outputs
、和Mapping
部分编写断言。
匹配器
的默认部分匹配行为hasResourceProperties
可以使用Match
类中的匹配器进行更改。
匹配器的范围从宽松 (Match.anyValue
) 到严格 ()。Match.objectEquals
它们可以嵌套,以便将不同的匹配方法应用于资源属性的不同部分。例如,Match.anyValue
结合使用Match.objectEquals
和,我们可以更全面地测试状态机的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基于亚马逊状态的语言中的字符串定义的。我们将使用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()); } } }
测试小贴士
请记住,您的测试将与它们测试的代码一样长,并且它们将被读取和修改的频率相同。因此,花点时间考虑如何最好地写出它们是值得的。
不要复制和粘贴设置行或常见断言。取而代之的是,将此逻辑重构为夹具或辅助函数。使用能反映每项测试实际测试内容的好名字。
不要试图在一次测试中做太多事情。最好是,测试应该只测试一种行为。如果你不小心破坏了这种行为,那么只有一个测试应该失败,测试的名称应该会告诉你失败了什么。但是,这更是值得努力实现的理想;有时你会不可避免地(或无意中)编写测试多个行为的测试。出于我们已经描述的原因,快照测试特别容易出现此问题,因此请谨慎使用它们。