这是 AWS CDK v2 开发者指南。旧版 CDK v1 于 2022 年 6 月 1 日进入维护阶段,并于 2023 年 6 月 1 日终止支持。
本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。
使用后 AWS CDK,您的基础架构可以像您编写的任何其他代码一样具有可测试性。你可以在云端和本地进行测试。本主题介绍如何在云端进行测试。有关本地测试的指导,请参阅使用在本地测试和构建 AWS CDK 应用程序 AWS SAM CLI。测试 AWS CDK 应用程序的标准方法使用断言模块和流行 AWS CDK的测试框架,例如 Jest fo JavaScript r and 和 Python 的 TypeScript Pyt
您可以为 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 创建的项目中的默认依赖项。但您必须安装测试框架。
$
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"]
}
}
示例堆栈
以下是本主题中将要测试的堆栈。如前所述,其包含了一个 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);
}
}
}
我们将修改应用程序的主入口点,这样我们就不会实际上将堆栈实例化。我们不想意外地将其部署。我们的测试将创建一个应用程序和一个用于测试的堆栈实例。与测试驱动开发结合使用时,这是一种有用的策略:确保在启用部署之前堆栈通过所有测试。
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.
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 应用程序时,可以在堆栈的构造函数中子类化 Stack
并实例化 Amazon SNS 主题。在我们的测试中,我们直接实例化 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
部分编写断言。
匹配程序
可以使用 Match
类中的匹配程序来更改 hasResourceProperties
的默认部分匹配行为。
匹配程序的范围从宽松 (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 Language 中的字符串定义的。我们将使用 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();
});
});
测试提示
请记住,您的测试持续时间将与其测试的代码一样长,且会被经常读取和修改。因此,有必要花时间考虑如何最好地编写测试。
请勿复制和粘贴设置行或常见断言。相反,应将此逻辑重构为夹具或辅助函数。使用能反映每项测试实际测试内容的有效名称。
请勿试图在一次测试中执行太多操作。一个测试最好只测试一种行为。如果您不小心破坏了这种行为,那么只有一个测试会失败,测试名称会告诉您哪一个测试失败。但是,这更像是一个值得努力实现的理想;有时您会不可避免地(或无意中)编写测试多个行为的测试。快照测试特别容易出现此问题,原因我们已经描述过,因此请谨慎使用快照测试。