Costrutti di test - AWS Cloud Development Kit (AWS CDK) v2

Questa è la guida per sviluppatori AWS CDK v2. Il vecchio CDK v1 è entrato in manutenzione il 1° giugno 2022 e ha terminato il supporto il 1° giugno 2023.

Le traduzioni sono generate tramite traduzione automatica. In caso di conflitto tra il contenuto di una traduzione e la versione originale in Inglese, quest'ultima prevarrà.

Costrutti di test

Con AWS CDK, la tua infrastruttura può essere testabile come qualsiasi altro codice che scrivi. L'approccio standard al test AWS CDK delle app utilizza il modulo assertions e i framework AWS CDK di test più diffusi come Jest for e/o Pytest for TypeScript JavaScript Python.

Esistono due categorie di test che puoi scrivere per le app. AWS CDK

  • Le asserzioni dettagliate verificano aspetti specifici del AWS CloudFormation modello generato, ad esempio «questa risorsa ha questa proprietà con questo valore». Questi test possono rilevare regressioni. Sono utili anche quando si sviluppano nuove funzionalità utilizzando lo sviluppo basato sui test. (Puoi prima scrivere un test, poi farlo passare scrivendo un'implementazione corretta.) Le asserzioni dettagliate sono i test più utilizzati.

  • I test snapshot testano il modello sintetizzato rispetto a un AWS CloudFormation modello di base precedentemente memorizzato. I test snapshot consentono di eseguire il refactoring liberamente, poiché si può essere certi che il codice refattorizzato funzioni esattamente allo stesso modo dell'originale. Se le modifiche sono state intenzionali, puoi accettare una nuova base per i test futuri. Tuttavia, gli aggiornamenti CDK possono anche causare la modifica dei modelli sintetizzati, quindi non potete affidarvi solo alle istantanee per assicurarvi che l'implementazione sia corretta.

Nota

Le versioni complete delle TypeScript app Python e Java utilizzate come esempi in questo argomento sono disponibili su. GitHub

Nozioni di base

Per illustrare come scrivere questi test, creeremo uno stack che contiene una macchina a AWS Step Functions stati e una funzione. AWS Lambda La funzione Lambda è sottoscritta a un argomento di Amazon SNS e inoltra semplicemente il messaggio alla macchina a stati.

Innanzitutto, crea un progetto di applicazione CDK vuoto utilizzando CDK Toolkit e installando le librerie di cui avremo bisogno. I costrutti che useremo si trovano tutti nel pacchetto CDK principale, che è una dipendenza predefinita nei progetti creati con CDK Toolkit. Tuttavia, è necessario installare il framework di test.

TypeScript
mkdir state-machine && cd state-machine cdk init --language=typescript npm install --save-dev jest @types/jest

Crea una directory per i tuoi test.

mkdir test

Modifica il progetto package.json per dire a NPM come eseguire Jest e per dire a Jest quali tipi di file raccogliere. Le modifiche necessarie sono le seguenti.

  • Aggiungere una nuova test chiave alla scripts sezione

  • Aggiungi Jest e i suoi tipi alla sezione devDependencies

  • Aggiungi una nuova chiave jest di primo livello con una dichiarazione moduleFileExtensions

Queste modifiche sono illustrate nello schema seguente. Posiziona il nuovo testo dove indicato inpackage.json. I segnaposto «...» indicano le parti esistenti del file che non devono essere modificate.

{ ... "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

Crea una cartella per i tuoi test.

mkdir test

Modifica il progetto package.json per dire a NPM come eseguire Jest e per dire a Jest quali tipi di file raccogliere. Le modifiche necessarie sono le seguenti.

  • Aggiungere una nuova test chiave alla scripts sezione

  • Aggiungi Jest alla sezione devDependencies

  • Aggiungi una nuova chiave jest di primo livello con una dichiarazione moduleFileExtensions

Queste modifiche sono illustrate nello schema seguente. Posiziona il nuovo testo dove indicato inpackage.json. I segnaposto «...» indicano parti esistenti del file che non devono essere modificate.

{ ... "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

Apri il progetto nel tuo IDE Java preferito. (In Eclipse, usa File > Importa > Progetti Maven esistenti.)

C#
mkdir state-machine && cd-state-machine cdk init --language=csharp

Apri src\StateMachine.sln in Visual Studio.

Fai clic con il pulsante destro del mouse sulla soluzione in Solution Explorer e scegli Aggiungi > Nuovo progetto. Cerca MSTest C# e aggiungi un progetto di test MSTest per C#. (Il nome predefinito va bene.) TestProject1

Fate clic con il pulsante destro del mouse TestProject1 e scegliete Aggiungi > Riferimento al StateMachine progetto, quindi aggiungete il progetto come riferimento.

Lo stack di esempio

Ecco lo stack che verrà testato in questo argomento. Come descritto in precedenza, contiene una funzione Lambda e una macchina a stati Step Functions e accetta uno o più argomenti di Amazon SNS. La funzione Lambda è sottoscritta agli argomenti di Amazon SNS e li inoltra alla macchina a stati.

Non devi fare nulla di speciale per rendere l'app testabile. In effetti, questo stack CDK non è diverso in alcun modo importante dagli altri stack di esempio in questa Guida.

TypeScript

Inserite il seguente codice in: 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

Inserisci il seguente codice inlib/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

Inserisci il seguente codice instate_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); } } } }

Modificheremo il punto di ingresso principale dell'app in modo da non creare effettivamente un'istanza del nostro stack. Non vogliamo distribuirlo accidentalmente. I nostri test creeranno un'app e un'istanza dello stack per i test. Questa è una tattica utile se combinata con lo sviluppo basato sui test: assicurati che lo stack superi tutti i test prima di abilitare l'implementazione.

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(); } } }

La funzione Lambda

Il nostro stack di esempio include una funzione Lambda che avvia la nostra macchina a stati. Dobbiamo fornire il codice sorgente per questa funzione in modo che il CDK possa raggrupparla e distribuirla come parte della creazione della risorsa della funzione Lambda.

  • Crea la cartella start-state-machine nella directory principale dell'app.

  • In questa cartella, crea almeno un file. Ad esempio, è possibile salvare il codice seguente instart-state-machines/index.js.

    exports.handler = async function (event, context) { return 'hello world'; };

    Tuttavia, qualsiasi file funzionerà, poiché in realtà non distribuiremo lo stack.

Esecuzione di test.

A titolo di riferimento, ecco i comandi che usi per eseguire i test nella tua AWS CDK app. Questi sono gli stessi comandi che useresti per eseguire i test in qualsiasi progetto utilizzando lo stesso framework di test. Per le lingue che richiedono una fase di compilazione, includila per assicurarti che i test siano stati compilati.

TypeScript
tsc && npm test
JavaScript
npm test
Python
python -m pytest
Java
mvn compile && mvn test
C#

Crea la tua soluzione (F6) per scoprire i test, quindi esegui i test (Test > Esegui tutti i test). Per scegliere quali test eseguire, apri Test Explorer (Test > Test Explorer).

O:

dotnet test src

Asserzioni granulari

Il primo passo per testare uno stack con asserzioni granulari è sintetizzare lo stack, perché stiamo scrivendo asserzioni sulla base del modello generato. AWS CloudFormation

StateMachineStackStackRichiediamo che venga passato l'argomento Amazon SNS per essere inoltrato alla macchina a stati. Quindi, nel nostro test, creeremo uno stack separato per contenere l'argomento.

In genere, quando si scrive un'app CDK, è possibile creare sottoclassi Stack e istanziare l'argomento Amazon SNS nel costruttore dello stack. Nel nostro test, creiamo Stack direttamente un'istanza, quindi passiamo questo stack come ambito e lo colleghiamo allo stack. Topic Questo è funzionalmente equivalente e meno prolisso. Aiuta anche a far sì che gli stack utilizzati solo nei test «abbiano un aspetto diverso» dagli stack che intendi distribuire.

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 } } }

Ora possiamo affermare che la funzione Lambda e l'abbonamento Amazon SNS sono stati creati.

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);

Il nostro test funzionale Lambda afferma che due proprietà particolari della risorsa funzionale hanno valori specifici. Per impostazione predefinita, il hasResourceProperties metodo esegue una corrispondenza parziale sulle proprietà della risorsa come indicato nel modello sintetizzato CloudFormation . Questo test richiede che le proprietà fornite esistano e abbiano i valori specificati, ma la risorsa può avere anche altre proprietà, che non sono state testate.

La nostra affermazione di Amazon SNS afferma che il modello sintetizzato contiene un abbonamento, ma nulla sull'abbonamento stesso. Abbiamo incluso questa affermazione principalmente per illustrare come affermare il numero di risorse. La Template classe offre metodi più specifici per scrivere asserzioni rispetto alle Mapping sezioni ResourcesOutputs, e del modello. CloudFormation

Matchers

Il comportamento di abbinamento parziale predefinito di hasResourceProperties può essere modificato utilizzando i matcher della Matchclasse.

I matcher vanno da lenient (Match.anyValue) a strict (). Match.objectEquals Possono essere annidati per applicare diversi metodi di corrispondenza a diverse parti delle proprietà della risorsa. Utilizzando Match.objectEquals e Match.anyValue insieme, ad esempio, possiamo testare in modo più completo il ruolo IAM della macchina a stati, senza richiedere valori specifici per le proprietà che potrebbero cambiare.

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" } } } } } } } } } } } } } }));

Molte CloudFormation risorse includono oggetti JSON serializzati rappresentati come stringhe. Il Match.serializedJson() matcher può essere utilizzato per abbinare le proprietà all'interno di questo JSON.

Ad esempio, le macchine a stati Step Functions vengono definite utilizzando una stringa in Amazon States Language basato su JSON. Lo useremo Match.serializedJson() per assicurarci che il nostro stato iniziale sia l'unico passaggio. Ancora una volta, useremo abbinatori annidati per applicare diversi tipi di corrispondenza a diverse parti dell'oggetto.

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() } }} }} }) )}});

Catturare

Spesso è utile testare le proprietà per assicurarsi che seguano formati specifici o abbiano lo stesso valore di un'altra proprietà, senza bisogno di conoscerne i valori esatti in anticipo. Il assertions modulo offre questa funzionalità nella sua Captureclasse.

Specificando un'Captureistanza al posto di un valore inhasResourceProperties, tale valore viene mantenuto nell'Captureoggetto. Il valore effettivo acquisito può essere recuperato utilizzando i as metodi dell'oggetto, tra cui, e asNumber() asString()asObject, e sottoposto a test. Da utilizzare Capture con un matcher per specificare la posizione esatta del valore da acquisire all'interno delle proprietà della risorsa, incluse le proprietà JSON serializzate.

L'esempio seguente verifica che lo stato iniziale della nostra macchina a stati abbia un nome che inizia con. Start Verifica inoltre che questo stato sia presente nell'elenco degli stati della macchina.

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()));

Test istantanei

Nei test delle istantanee, si confronta l'intero modello sintetizzato con un CloudFormation modello di base precedentemente memorizzato (spesso chiamato «master»). A differenza delle asserzioni granulari, il test delle istantanee non è utile per rilevare le regressioni. Questo perché il test delle istantanee si applica all'intero modello e altre cose, oltre alle modifiche al codice, possono causare piccole (o) differenze nei risultati di sintesi. not-so-small Queste modifiche potrebbero non influire nemmeno sulla distribuzione, ma causeranno comunque il fallimento di un test di snapshot.

Ad esempio, è possibile aggiornare un costrutto CDK per incorporare una nuova best practice, che può causare modifiche alle risorse sintetizzate o al modo in cui sono organizzate. In alternativa, è possibile aggiornare il CDK Toolkit a una versione che riporti metadati aggiuntivi. Le modifiche ai valori di contesto possono influire anche sul modello sintetizzato.

Tuttavia, i test istantanei possono essere di grande aiuto nel refactoring, purché si mantengano costanti tutti gli altri fattori che potrebbero influenzare il modello sintetizzato. Saprai immediatamente se una modifica apportata ha modificato involontariamente il modello. Se la modifica è intenzionale, è sufficiente accettare il nuovo modello come riferimento.

Ad esempio, se abbiamo questo DeadLetterQueue costrutto:

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() }); } } }

Possiamo testarlo in questo modo:

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()); } } }

Suggerimenti per i test

Ricorda che i tuoi test dureranno tanto quanto il codice che testano e verranno letti e modificati con la stessa frequenza. Pertanto, vale la pena dedicare un momento a considerare il modo migliore per scriverli.

Non copiate e incollate righe di configurazione o asserzioni comuni. Invece, rifattorizza questa logica in dispositivi o funzioni di supporto. Usa nomi validi che riflettano ciò che ogni test effettivamente verifica.

Non cercare di fare troppe cose in un solo test. Preferibilmente, un test dovrebbe testare uno e un solo comportamento. Se si interrompe accidentalmente tale comportamento, dovrebbe fallire esattamente un test e il nome del test dovrebbe indicare cosa non è riuscito. Tuttavia, questo è più un ideale da perseguire; a volte inevitabilmente (o inavvertitamente) si scrivono test che testano più di un comportamento. I test istantanei, per i motivi che abbiamo già descritto, sono particolarmente inclini a questo problema, quindi usateli con parsimonia.