Dies ist der AWS CDK v2-Entwicklerhandbuch. Die ältere CDK Version 1 wurde am 1. Juni 2022 in die Wartung aufgenommen und der Support wurde am 1. Juni 2023 eingestellt.
Die vorliegende Übersetzung wurde maschinell erstellt. Im Falle eines Konflikts oder eines Widerspruchs zwischen dieser übersetzten Fassung und der englischen Fassung (einschließlich infolge von Verzögerungen bei der Übersetzung) ist die englische Fassung maßgeblich.
AWS CDK Anwendungen testen
Mit dem AWS CDK kann Ihre Infrastruktur genauso testbar sein wie jeder andere Code, den Sie schreiben. Der Standardansatz zum Testen von AWS CDK Apps verwendet AWS CDK das Assertions-Modul und beliebte Testframeworks wie Jest for JavaScript und/oder TypeScript Pytest for Python.
Es gibt zwei Kategorien von Tests, die Sie für Apps schreiben können. AWS CDK
-
Präzise Assertionen testen bestimmte Aspekte der generierten AWS CloudFormation Vorlage, z. B. „Diese Ressource hat diese Eigenschaft mit diesem Wert“. Mit diesen Tests können Regressionen erkannt werden. Sie sind auch nützlich, wenn Sie mithilfe der testgetriebenen Entwicklung neue Funktionen entwickeln. (Sie können zuerst einen Test schreiben und ihn dann bestehen lassen, indem Sie eine korrekte Implementierung schreiben.) Feinkörnige Assertionen sind die am häufigsten verwendeten Tests.
-
Snapshot-Tests testen das synthetisierte AWS CloudFormation Template mit einem zuvor gespeicherten Baseline-Template. Mit Snapshot-Tests können Sie frei umgestalten, da Sie sicher sein können, dass der umgestaltete Code genauso funktioniert wie das Original. Wenn die Änderungen beabsichtigt waren, können Sie eine neue Ausgangsbasis für zukünftige Tests akzeptieren. CDKUpgrades können jedoch auch dazu führen, dass sich synthetisierte Vorlagen ändern, sodass Sie sich nicht nur auf Snapshots verlassen können, um sicherzustellen, dass Ihre Implementierung korrekt ist.
Vollständige Versionen der TypeScript Python- und Java-Apps, die in diesem Thema als Beispiele verwendet werden, sind unter verfügbar GitHub.
Erste Schritte
Um zu veranschaulichen, wie diese Tests geschrieben werden, erstellen wir einen Stack, der eine AWS Step Functions Zustandsmaschine und eine AWS Lambda Funktion enthält. Die Lambda-Funktion abonniert ein SNS Amazon-Thema und leitet die Nachricht einfach an die Zustandsmaschine weiter.
Erstellen Sie zunächst mit dem CDK Toolkit ein leeres CDK Anwendungsprojekt und installieren Sie die benötigten Bibliotheken. Die Konstrukte, die wir verwenden werden, befinden sich alle im CDK Hauptpaket, was eine Standardabhängigkeit in Projekten ist, die mit dem CDK Toolkit erstellt wurden. Sie müssen jedoch Ihr Test-Framework installieren.
- TypeScript
-
mkdir state-machine && cd state-machine
cdk init --language=typescript
npm install --save-dev jest @types/jest
Erstellen Sie ein Verzeichnis für Ihre Tests.
mkdir test
Bearbeiten Sie die Projektepackage.json
, um festzulegen, NPM wie Jest ausgeführt werden soll, und um Jest mitzuteilen, welche Arten von Dateien gesammelt werden sollen. Die erforderlichen Änderungen lauten wie folgt.
-
Fügen Sie dem scripts
Abschnitt einen neuen test
Schlüssel hinzu
-
Fügen Sie Jest und seine Typen zum devDependencies
Abschnitt hinzu
-
Fügen Sie einen neuen Schlüssel der jest
obersten Ebene mit einer Deklaration hinzu moduleFileExtensions
Diese Änderungen werden in der folgenden Übersicht dargestellt. Platzieren Sie den neuen Text an der unter angegebenen Stellepackage.json
. Die Platzhalter „...“ stehen für vorhandene Teile der Datei, die nicht geändert werden sollten.
{
...
"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
Erstellen Sie ein Verzeichnis für Ihre Tests.
mkdir test
Bearbeiten Sie die Projektepackage.json
, um festzulegen, NPM wie Jest ausgeführt werden soll, und um Jest mitzuteilen, welche Arten von Dateien gesammelt werden sollen. Die erforderlichen Änderungen lauten wie folgt.
-
Fügen Sie dem scripts
Abschnitt einen neuen test
Schlüssel hinzu
-
Füge Jest zum Abschnitt hinzu devDependencies
-
Fügen Sie einen neuen Schlüssel der jest
obersten Ebene mit einer Deklaration hinzu moduleFileExtensions
Diese Änderungen werden in der folgenden Übersicht dargestellt. Platzieren Sie den neuen Text an der unter angegebenen Stellepackage.json
. Die Platzhalter „...“ stehen für vorhandene Teile der Datei, die nicht geändert werden sollten.
{
...
"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
Öffnen Sie das Projekt in Ihrem bevorzugten JavaIDE. (Verwenden Sie in Eclipse „Datei“ > „Importieren“ > „Bestehende Maven-Projekte“.)
- C#
-
mkdir state-machine && cd-state-machine
cdk init --language=csharp
src\StateMachine.sln
In Visual Studio öffnen.
Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf die Lösung und wählen Sie Hinzufügen > Neues Projekt. Suchen Sie nach MSTest C# und fügen Sie ein MSTestTestprojekt für C# hinzu. (Der Standardname TestProject1
ist in Ordnung.)
Klicken Sie mit der rechten MaustasteTestProject1
, wählen Sie Hinzufügen > Projektreferenz und fügen Sie das StateMachine
Projekt als Referenz hinzu.
Der Beispielstapel
Hier ist der Stack, der in diesem Thema getestet wird. Wie bereits beschrieben, enthält es eine Lambda-Funktion und eine Step Functions Functions-Zustandsmaschine und akzeptiert ein oder mehrere SNS Amazon-Themen. Die Lambda-Funktion abonniert die SNS Amazon-Themen und leitet sie an die State Machine weiter.
Sie müssen nichts Besonderes tun, um die App testbar zu machen. Tatsächlich unterscheidet sich dieser CDK Stapel in keiner wichtigen Weise von den anderen Beispielstapeln in diesem Handbuch.
- TypeScript
-
Fügen Sie den folgenden Code ein: 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
-
Geben Sie den folgenden Code einlib/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
-
Geben Sie den folgenden Code einstate_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);
}
}
}
}
Wir werden den Haupteinstiegspunkt der App so ändern, dass wir unseren Stack nicht wirklich instanziieren. Wir wollen es nicht versehentlich bereitstellen. Unsere Tests werden eine App und eine Instanz des Stacks zum Testen erstellen. In Kombination mit testgetriebener Entwicklung ist dies eine nützliche Taktik: Stellen Sie sicher, dass der Stack alle Tests besteht, bevor Sie die Bereitstellung aktivieren.
- 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();
}
}
}
Die Lambda-Funktion
Unser Beispielstapel enthält eine Lambda-Funktion, die unsere Zustandsmaschine startet. Wir müssen den Quellcode für diese Funktion bereitstellen, damit sie ihn als Teil der Erstellung der Lambda-Funktionsressource bündeln und bereitstellen CDK können.
-
Erstellen Sie den Ordner start-state-machine
im Hauptverzeichnis der App.
-
Erstellen Sie in diesem Ordner mindestens eine Datei. Sie können beispielsweise den folgenden Code in speichernstart-state-machines/index.js
.
exports.handler = async function (event, context) {
return 'hello world';
};
Es wird jedoch jede Datei funktionieren, da wir den Stack nicht wirklich bereitstellen werden.
Ausführen von Tests
Als Referenz finden Sie hier die Befehle, mit denen Sie Tests in Ihrer AWS CDK App ausführen. Dies sind dieselben Befehle, die Sie verwenden würden, um die Tests in jedem Projekt mit demselben Testframework auszuführen. Fügen Sie bei Sprachen, die einen Build-Schritt erfordern, diesen hinzu, um sicherzustellen, dass Ihre Tests kompiliert wurden.
- TypeScript
-
tsc && npm test
- JavaScript
-
npm test
- Python
-
python -m pytest
- Java
-
mvn compile && mvn test
- C#
-
Erstellen Sie Ihre Lösung (F6), um die Tests zu ermitteln, und führen Sie dann die Tests aus (Test > Alle Tests ausführen). Um auszuwählen, welche Tests ausgeführt werden sollen, öffnen Sie den Test Explorer (Test > Test Explorer).
Oder:
dotnet test src
Fein abgestufte Behauptungen
Der erste Schritt zum Testen eines Stacks mit feinkörnigen Assertionen besteht darin, den Stack zu synthetisieren, da wir Assertionen anhand der generierten Vorlage schreiben. AWS CloudFormation
Unser StateMachineStackStack
verlangt, dass wir ihm das SNS Amazon-Thema übergeben, damit es an die Zustandsmaschine weitergeleitet wird. In unserem Test erstellen wir also einen separaten Stapel, der das Thema enthält.
Normalerweise können Sie beim Schreiben einer CDK App das SNS Amazon-Thema im Konstruktor des Stacks unterordnen Stack
und instanziieren. In unserem Test instanziieren wir Stack
direkt, übergeben diesen Stack dann als Gültigkeitsbereich und hängen ihn an den Topic
Stack an. Das ist funktionell äquivalent und weniger ausführlich. Es trägt auch dazu bei, dass Stacks, die nur in Tests verwendet werden, „anders aussehen“ als die Stacks, die Sie bereitstellen möchten.
- 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
}
}
}
Jetzt können wir behaupten, dass die Lambda-Funktion und das SNS Amazon-Abonnement erstellt wurden.
- 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);
Unser Lambda-Funktionstest bestätigt, dass zwei bestimmte Eigenschaften der Funktionsressource spezifische Werte haben. Standardmäßig führt die hasResourceProperties
Methode eine teilweise Übereinstimmung der Eigenschaften der Ressource durch, wie sie in der synthetisierten CloudFormation Vorlage angegeben sind. Dieser Test setzt voraus, dass die angegebenen Eigenschaften vorhanden sind und die angegebenen Werte haben. Die Ressource kann jedoch auch andere Eigenschaften haben, die nicht getestet wurden.
Unsere SNS Behauptung von Amazon behauptet, dass die synthetisierte Vorlage ein Abonnement enthält, aber nichts über das Abonnement selbst. Wir haben diese Behauptung hauptsächlich aufgenommen, um zu veranschaulichen, wie die Anzahl der Ressourcen bestätigt werden kann. Die Template
Klasse bietet spezifischere Methoden, um Assertions für die Mapping
Abschnitte Resources
Outputs
, und der CloudFormation Vorlage zu schreiben.
Matcher
Das standardmäßige Verhalten bei teilweisem Abgleich von hasResourceProperties
kann mithilfe von Matchern aus der Match
Klasse geändert werden.
Die Übereinstimmungen reichen von lenient (Match.anyValue
) bis strikt (). Match.objectEquals
Sie können verschachtelt werden, um unterschiedliche Abgleichmethoden auf verschiedene Teile der Ressourceneigenschaften anzuwenden. Mithilfe von Match.objectEquals
und Match.anyValue
zusammen können wir beispielsweise die IAM Rolle der Zustandsmaschine umfassender testen, ohne dass spezifische Werte für Eigenschaften erforderlich sind, die sich ändern könnten.
- 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" }
}
}
}
}
}
}
}
}
}
}
}
}
}));
Viele CloudFormation Ressourcen enthalten serialisierte JSON Objekte, die als Zeichenketten dargestellt werden. Der Match.serializedJson()
Matcher kann verwendet werden, um die darin enthaltenen Eigenschaften abzugleichen. JSON
Beispielsweise werden Step Functions Functions-Zustandsmaschinen mithilfe einer Zeichenfolge in der JSON Amazon States-Sprache definiert. Damit stellen wir sicherMatch.serializedJson()
, dass unser Ausgangsstatus der einzige Schritt ist. Auch hier verwenden wir verschachtelte Matcher, um verschiedene Arten von Matching auf verschiedene Teile des Objekts anzuwenden.
- 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() }
}}
}}
})
)}});
Erfassen
Es ist oft nützlich, Eigenschaften zu testen, um sicherzustellen, dass sie bestimmten Formaten entsprechen oder denselben Wert wie eine andere Eigenschaft haben, ohne ihre genauen Werte im Voraus kennen zu müssen. Das assertions
Modul bietet diese Funktion in seiner Capture
Klasse.
Durch die Angabe einer Capture
Instanz anstelle eines Werts in hasResourceProperties
wird dieser Wert im Capture
Objekt beibehalten. Der tatsächlich erfasste Wert kann mit den as
Methoden des Objekts, einschließlichasNumber()
, und asString()
asObject
, abgerufen und einem Test unterzogen werden. Verwenden Sie Capture
ihn zusammen mit einem Matcher, um die genaue Position des zu erfassenden Werts innerhalb der Eigenschaften der Ressource, einschließlich serialisierter JSON Eigenschaften, anzugeben.
Das folgende Beispiel testet, ob der Startstatus unserer Zustandsmaschine einen Namen hat, der mit beginnt. Start
Es testet auch, ob dieser Status in der Liste der Zustände in der Maschine vorhanden ist.
- 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()));
Snapshot-Tests
Beim Snapshot-Testen vergleichen Sie die gesamte synthetisierte CloudFormation Vorlage mit einer zuvor gespeicherten Baseline-Vorlage (oft als „Master“ bezeichnet). Im Gegensatz zu feinkörnigen Behauptungen sind Snapshot-Tests nicht nützlich, um Regressionen abzufangen. Das liegt daran, dass Snapshot-Tests für die gesamte Vorlage gelten und Dinge außer Codeänderungen zu kleinen (oder not-so-small) Unterschieden in den Syntheseergebnissen führen können. Diese Änderungen wirken sich möglicherweise nicht einmal auf Ihre Bereitstellung aus, führen aber dennoch dazu, dass ein Snapshot-Test fehlschlägt.
Sie könnten beispielsweise ein CDK Konstrukt aktualisieren, um eine neue bewährte Methode zu integrieren, was zu Änderungen an den synthetisierten Ressourcen oder deren Organisation führen kann. Alternativ können Sie das CDK Toolkit auf eine Version aktualisieren, die zusätzliche Metadaten meldet. Änderungen an den Kontextwerten können sich auch auf die synthetisierte Vorlage auswirken.
Snapshot-Tests können jedoch beim Refactoring eine große Hilfe sein, solange Sie alle anderen Faktoren, die sich auf das synthetisierte Template auswirken könnten, konstant halten. Sie werden sofort wissen, ob eine Änderung, die Sie vorgenommen haben, die Vorlage unbeabsichtigt verändert hat. Wenn die Änderung beabsichtigt ist, akzeptieren Sie einfach die neue Vorlage als Grundlage.
Wenn wir zum Beispiel dieses DeadLetterQueue
Konstrukt haben:
- 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()
});
}
}
}
Wir können es so testen:
- 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());
}
}
}
Tipps für Tests
Denken Sie daran, dass Ihre Tests genauso lange gültig sind wie der Code, den sie testen, und sie werden genauso oft gelesen und geändert. Daher lohnt es sich, sich einen Moment Zeit zu nehmen, um zu überlegen, wie man sie am besten schreibt.
Kopieren Sie keine Setup-Zeilen oder allgemeine Behauptungen und fügen Sie sie nicht ein. Refaktorieren Sie diese Logik stattdessen in Fixtures oder Hilfsfunktionen. Verwenden Sie aussagekräftige Namen, die widerspiegeln, was jeder Test tatsächlich testet.
Versuchen Sie nicht, in einem Test zu viel zu tun. Vorzugsweise sollte ein Test nur ein Verhalten testen. Wenn Sie dieses Verhalten versehentlich brechen, sollte genau ein Test fehlschlagen, und der Name des Tests sollte Ihnen sagen, was fehlgeschlagen ist. Dies ist jedoch eher ein Ideal, das angestrebt werden sollte. Manchmal werden Sie unweigerlich (oder versehentlich) Tests schreiben, die mehr als ein Verhalten testen. Snapshot-Tests sind aus Gründen, die wir bereits beschrieben haben, besonders anfällig für dieses Problem. Verwenden Sie sie daher sparsam.