AWS CDK Applications de test - AWS Cloud Development Kit (AWS CDK) v2

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.

Note

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 ResourcesOutputs, 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 Matchclasse.

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

En spécifiant une Capture instance à la place d'une valeur inhasResourceProperties, cette valeur est conservée dans l'Captureobjet. 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.