Ceci est le guide du AWS CDK développeur de la version 2. L'ancienne CDK version 1 est entrée en maintenance le 1er juin 2022 et a pris fin le 1er juin 2023.
Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.
AWS CDK Applications de test
Avec le AWS CDK, votre infrastructure peut être aussi testable que n'importe quel autre code que vous écrivez. L'approche standard pour tester AWS CDK les applications utilise le module AWS CDK d'assertions et des frameworks de test populaires tels que Jest pour JavaScript et/ou TypeScript Pytest pour Python.
Il existe deux catégories de tests que vous pouvez écrire pour les AWS CDK applications.
-
Des assertions détaillées testent des aspects spécifiques du AWS CloudFormation modèle généré, tels que « cette ressource possède cette propriété avec cette valeur ». Ces tests permettent de détecter des régressions. Ils sont également utiles lorsque vous développez de nouvelles fonctionnalités à l'aide du développement piloté par les tests. (Vous pouvez d'abord écrire un test, puis le réussir en écrivant une implémentation correcte.) Les assertions précises sont les tests les plus fréquemment utilisés.
-
Les tests instantanés testent le AWS CloudFormation modèle synthétisé par rapport à un modèle de référence précédemment stocké. Les tests instantanés vous permettent de refactoriser librement, car vous pouvez être sûr que le code refactorisé fonctionne exactement de la même manière que le code original. Si les modifications étaient intentionnelles, vous pouvez accepter une nouvelle référence pour les futurs tests. Toutefois, les CDK mises à niveau peuvent également entraîner la modification des modèles synthétisés. Vous ne pouvez donc pas vous fier uniquement aux instantanés pour vous assurer que votre implémentation est correcte.
Les versions complètes TypeScript des applications Python et Java utilisées comme exemples dans cette rubrique sont disponibles sur GitHub.
Premiers pas
Pour illustrer comment écrire ces tests, nous allons créer une pile contenant une machine à AWS Step Functions états et une AWS Lambda fonction. La fonction Lambda est abonnée à une SNS rubrique Amazon et transmet simplement le message à la machine à états.
Tout d'abord, créez un projet d'CDKapplication vide à l'aide du CDK Toolkit et en installant les bibliothèques dont nous aurons besoin. Les constructions que nous utiliserons se trouvent toutes dans le CDK package principal, qui est une dépendance par défaut dans les projets créés avec le CDK Toolkit. Cependant, vous devez installer votre framework de test.
- TypeScript
-
mkdir state-machine && cd state-machine
cdk init --language=typescript
npm install --save-dev jest @types/jest
Créez un répertoire pour vos tests.
mkdir test
Modifiez les projets package.json
pour indiquer NPM comment exécuter Jest et pour indiquer à Jest quels types de fichiers collecter. Les modifications nécessaires sont les suivantes.
-
Ajouter une nouvelle test
clé à la scripts
section
-
Ajoutez Jest et ses types à la section devDependencies
-
Ajouter une nouvelle clé jest
de niveau supérieur avec une déclaration moduleFileExtensions
Ces modifications sont présentées dans le schéma suivant. Placez le nouveau texte à l'endroit indiqué danspackage.json
. Les espaces réservés «... » indiquent les parties existantes du fichier qui ne doivent pas être modifiées.
{
...
"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
Créez un répertoire pour vos tests.
mkdir test
Modifiez les projets package.json
pour indiquer NPM comment exécuter Jest et pour indiquer à Jest quels types de fichiers collecter. Les modifications nécessaires sont les suivantes.
-
Ajouter une nouvelle test
clé à la scripts
section
-
Ajoutez Jest à la section devDependencies
-
Ajouter une nouvelle clé jest
de niveau supérieur avec une déclaration moduleFileExtensions
Ces modifications sont présentées dans le schéma suivant. Placez le nouveau texte à l'endroit indiqué danspackage.json
. Les espaces réservés «... » indiquent les parties existantes du fichier qui ne doivent pas être modifiées.
{
...
"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
Ouvrez le projet dans le langage Java de votre choixIDE. (Dans Eclipse, utilisez Fichier > Importer > Projets Maven existants.)
- C#
-
mkdir state-machine && cd-state-machine
cdk init --language=csharp
Ouvrez src\StateMachine.sln
dans Visual Studio.
Cliquez avec le bouton droit sur la solution dans l'Explorateur de solutions et choisissez Ajouter > Nouveau projet. Recherchez MSTest C# et ajoutez un projet de MSTest test pour C#. (Le nom par défaut TestProject1
est correct.)
Cliquez avec le bouton droit de la souris TestProject1
et choisissez Ajouter > Référence du StateMachine
projet, puis ajoutez le projet comme référence.
La pile d'exemples
Voici la pile qui sera testée dans cette rubrique. Comme nous l'avons décrit précédemment, il contient une fonction Lambda et une machine à états Step Functions, et accepte une ou plusieurs rubriques AmazonSNS. La fonction Lambda est abonnée aux SNS rubriques Amazon et les transmet à la machine à états.
Vous n'avez rien à faire de spécial pour rendre l'application testable. En fait, cette CDK pile n'est pas différente des autres exemples de piles présentés dans ce guide.
- TypeScript
-
Entrez le code suivant dans 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
-
Entrez le code suivant dans 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
-
Entrez le code suivant dans 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);
}
}
}
}
Nous allons modifier le point d'entrée principal de l'application afin de ne pas réellement instancier notre stack. Nous ne voulons pas le déployer accidentellement. Nos tests créeront une application et une instance de la pile à des fins de test. Cette tactique est utile lorsqu'elle est associée au développement piloté par les tests : assurez-vous que la pile passe tous les tests avant d'activer le déploiement.
- TypeScript
-
Dans 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
-
Dans 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
-
Dans 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();
}
}
}
La fonction Lambda
Notre exemple de pile inclut une fonction Lambda qui démarre notre machine à états. Nous devons fournir le code source de cette fonction afin qu'ils CDK puissent la regrouper et la déployer dans le cadre de la création de la ressource de fonction Lambda.
-
Créez le dossier start-state-machine
dans le répertoire principal de l'application.
-
Dans ce dossier, créez au moins un fichier. Par exemple, vous pouvez enregistrer le code suivant dansstart-state-machines/index.js
.
exports.handler = async function (event, context) {
return 'hello world';
};
Cependant, n'importe quel fichier fonctionnera, car nous ne déploierons pas réellement la pile.
Exécution de tests
À titre de référence, voici les commandes que vous utilisez pour exécuter des tests dans votre AWS CDK application. Il s'agit des mêmes commandes que vous utiliseriez pour exécuter les tests dans n'importe quel projet utilisant le même framework de test. Pour les langages qui nécessitent une étape de compilation, incluez-la pour vous assurer que vos tests ont été compilés.
- TypeScript
-
tsc && npm test
- JavaScript
-
npm test
- Python
-
python -m pytest
- Java
-
mvn compile && mvn test
- C#
-
Créez votre solution (F6) pour découvrir les tests, puis exécutez-les (Test > Exécuter tous les tests). Pour choisir les tests à exécuter, ouvrez l'Explorateur de tests (Test > Explorateur de tests).
Ou:
dotnet test src
Assertions fines
La première étape pour tester une pile avec des assertions détaillées consiste à synthétiser la pile, car nous écrivons des assertions par rapport au modèle généré. AWS CloudFormation
Nous StateMachineStackStack
exigeons que nous lui transmettions le SNS sujet Amazon pour qu'il soit transféré à la machine d'état. Dans notre test, nous allons donc créer une pile séparée pour contenir le sujet.
Normalement, lorsque vous écrivez une CDK application, vous pouvez sous-classer Stack
et instancier le sujet SNS Amazon dans le constructeur de la pile. Dans notre test, nous instancions Stack
directement, puis nous transmettons cette pile comme scope, en Topic
l'attachant à la pile. C'est équivalent sur le plan fonctionnel et moins verbeux. Cela permet également de rendre les piles utilisées uniquement dans les tests « différentes » des piles que vous avez l'intention de déployer.
- 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
}
}
}
Nous pouvons maintenant affirmer que la fonction Lambda et l'SNSabonnement Amazon ont été créés.
- 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);
Notre test de fonction Lambda affirme que deux propriétés particulières de la ressource fonctionnelle ont des valeurs spécifiques. Par défaut, la hasResourceProperties
méthode effectue une correspondance partielle sur les propriétés de la ressource telles qu'elles sont indiquées dans le CloudFormation modèle synthétisé. Ce test nécessite que les propriétés fournies existent et aient les valeurs spécifiées, mais la ressource peut également avoir d'autres propriétés, qui ne sont pas testées.
Notre SNS assertion Amazon affirme que le modèle synthétisé contient un abonnement, mais rien sur l'abonnement lui-même. Nous avons inclus cette assertion principalement pour illustrer comment affirmer le nombre de ressources. La Template
classe propose des méthodes plus spécifiques pour écrire des assertions par rapport aux Mapping
sections Resources
Outputs
, et du CloudFormation modèle.
Allumeurs
Le comportement de correspondance partielle par défaut de hasResourceProperties
peut être modifié à l'aide des matchers de la Match
classe.
Les matchers vont de indulgent (Match.anyValue
) à strict (Match.objectEquals
). Ils peuvent être imbriqués pour appliquer différentes méthodes de correspondance aux différentes parties des propriétés des ressources. En utilisant Match.objectEquals
et Match.anyValue
ensemble, par exemple, nous pouvons tester le IAM rôle de la machine à états de manière plus complète, sans avoir besoin de valeurs spécifiques pour les propriétés susceptibles de changer.
- 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" }
}
}
}
}
}
}
}
}
}
}
}
}
}));
De nombreuses CloudFormation ressources incluent des JSON objets sérialisés représentés sous forme de chaînes. Le Match.serializedJson()
matcher peut être utilisé pour faire correspondre les propriétés qu'il contientJSON.
Par exemple, les machines à états Step Functions sont définies à l'aide d'une chaîne JSON basée sur Amazon States Language. Nous allons nous Match.serializedJson()
assurer que notre état initial est la seule étape. Encore une fois, nous utiliserons des matchers imbriqués pour appliquer différents types de correspondance aux différentes parties de l'objet.
- 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() }
}}
}}
})
)}});
Capture
Il est souvent utile de tester les propriétés pour s'assurer qu'elles suivent des formats spécifiques ou qu'elles ont la même valeur qu'une autre propriété, sans avoir besoin de connaître leurs valeurs exactes à l'avance. Le assertions
module fournit cette fonctionnalité dans sa Capture
classe.
En spécifiant une Capture
instance à la place d'une valeur inhasResourceProperties
, cette valeur est conservée dans l'Capture
objet. La valeur capturée réelle peut être récupérée à l'aide as
des méthodes de l'objet asNumber()
asString()
, notammentasObject
, et soumise à un test. À utiliser Capture
avec un comparateur pour spécifier l'emplacement exact de la valeur à capturer dans les propriétés de la ressource, y compris les propriétés sérialiséesJSON.
L'exemple suivant teste pour s'assurer que l'état de départ de notre machine à états porte un nom commençant parStart
. Il vérifie également que cet état est présent dans la liste des états de la machine.
- 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()));
Tests instantanés
Lors des tests instantanés, vous comparez l'intégralité du CloudFormation modèle synthétisé à un modèle de référence précédemment stocké (souvent appelé « modèle principal »). Contrairement aux assertions détaillées, les tests instantanés ne sont pas utiles pour détecter les régressions. Cela est dû au fait que les tests instantanés s'appliquent à l'ensemble du modèle et que d'autres éléments que les modifications de code peuvent entraîner de légères (ou not-so-small) différences dans les résultats de synthèse. Ces modifications n'affecteront peut-être même pas votre déploiement, mais elles entraîneront tout de même l'échec d'un test de capture instantanée.
Par exemple, vous pouvez mettre à jour une CDK structure pour y intégrer une nouvelle bonne pratique, ce qui peut entraîner des modifications des ressources synthétisées ou de la façon dont elles sont organisées. Vous pouvez également mettre à jour le CDK kit d'outils vers une version qui indique des métadonnées supplémentaires. Les modifications apportées aux valeurs de contexte peuvent également affecter le modèle synthétisé.
Les tests instantanés peuvent toutefois être d'une grande aide pour le refactoring, à condition de maintenir constants tous les autres facteurs susceptibles d'affecter le modèle synthétisé. Vous saurez immédiatement si une modification que vous avez apportée a involontairement modifié le modèle. Si le changement est intentionnel, il suffit d'accepter le nouveau modèle comme base de référence.
Par exemple, si nous avons cette DeadLetterQueue
construction :
- 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()
});
}
}
}
Nous pouvons le tester comme ceci :
- 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());
}
}
}
Conseils pour les tests
N'oubliez pas que vos tests dureront aussi longtemps que le code qu'ils testent, et qu'ils seront lus et modifiés aussi souvent. Par conséquent, il vaut la peine de prendre un moment pour réfléchir à la meilleure façon de les écrire.
Ne copiez pas et ne collez pas de lignes de configuration ou d'assertions courantes. Refactorisez plutôt cette logique en accessoires ou en fonctions auxiliaires. Utilisez de bons noms qui reflètent ce que chaque test teste réellement.
N'essayez pas d'en faire trop en un seul test. De préférence, un test ne doit tester qu'un seul comportement. Si vous rompez accidentellement ce comportement, un seul test doit échouer, et le nom du test doit vous indiquer ce qui a échoué. Cependant, il s'agit plutôt d'un idéal à atteindre ; il arrive que vous écriviez inévitablement (ou par inadvertance) des tests qui testent plus d'un comportement. Pour les raisons que nous avons déjà décrites, les tests instantanés sont particulièrement sujets à ce problème. Utilisez-les donc avec parcimonie.