Prueba de aplicaciones de AWS CDK - AWS Cloud Development Kit (AWS CDK) v2

Esta es la guía para AWS CDK desarrolladores de la versión 2. La CDK versión anterior entró en mantenimiento el 1 de junio de 2022 y finalizó el soporte el 1 de junio de 2023.

Las traducciones son generadas a través de traducción automática. En caso de conflicto entre la traducción y la version original de inglés, prevalecerá la version en inglés.

Prueba de aplicaciones de AWS CDK

Gracias a AWS CDK, su infraestructura puede probarse como cualquier otro código que escriba. El enfoque estándar para probar aplicaciones de AWS CDK consiste en utilizar el módulo de aserciones de AWS CDK y los marcos de prueba populares, como Jest para TypeScript y JavaScript o Pytest para Python.

Hay dos categorías de pruebas que puede escribir para aplicaciones de AWS CDK.

  • Las aserciones detalladas prueban aspectos específicos de la plantilla de AWS CloudFormation generada, como “este recurso tiene esta propiedad con este valor”. Estas pruebas pueden detectar regresiones. Además, son útiles cuando se desarrollan características nuevas mediante desarrollo basado en pruebas. (Puede escribir una prueba primero y luego, escribir la implementación correcta para aprobarla). Las aserciones detalladas son las pruebas que se utilizan con más frecuencia.

  • Las pruebas de instantáneas prueban la plantilla de AWS CloudFormation sintetizada con una plantilla de referencia previamente almacenada. Las pruebas de instantáneas permiten refactorizar con libertad, ya que brindan la tranquilidad de que el código refactorizado funciona exactamente igual que el original. Si los cambios eran intencionales, puede aceptar un punto de referencia nuevo para pruebas futuras. Sin embargo, las actualizaciones de CDK también pueden provocar cambios en las plantillas sintetizadas; por lo tanto, no deposite toda su confianza en las instantáneas para asegurarse de que la implementación es correcta.

nota

Las versiones completas de las aplicaciones de TypeScript, Python y Java utilizadas como ejemplos en este tema están disponibles en GitHub.

Introducción

Para ilustrar cómo se escriben estas pruebas, crearemos una pila que contenga una máquina de estado de AWS Step Functions y una función de AWS Lambda. La función de Lambda está suscrita a un tema de Amazon SNS y simplemente reenvía el mensaje a la máquina de estado.

Primero, cree un proyecto de aplicación de CDK vacío con el kit de herramientas de CDK e instale las bibliotecas necesarias. Todos los constructos que utilizaremos se encuentran en el paquete principal de CDK, que es una dependencia predeterminada en los proyectos creados con el kit de herramientas de CDK. Sin embargo, debe instalar su marco de prueba.

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

Cree un directorio para las pruebas.

mkdir test

Edite el package.json del proyecto para indicar al NPM cómo ejecutar Jest e indicarle a Jest qué tipos de archivos se deben recopilar. Se deben implementar los siguientes cambios.

  • Agregue una clave test nueva a la sección scripts.

  • Agregue Jest y sus tipos a la sección devDependencies.

  • Agregue una clave jest nueva de nivel superior con una declaración moduleFileExtensions.

Estos cambios se muestran en el siguiente esquema. Coloque el texto nuevo donde se indica en package.json. Los marcadores de posición “…” indican las partes existentes del archivo que no deben modificarse.

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

Cree un directorio para las pruebas.

mkdir test

Edite el package.json del proyecto para indicar al NPM cómo ejecutar Jest e indicarle a Jest qué tipos de archivos se deben recopilar. Se deben implementar los siguientes cambios.

  • Agregue una clave test nueva a la sección scripts.

  • Agregue Jest a la sección devDependencies

  • Agregue una clave jest nueva de nivel superior con una declaración moduleFileExtensions.

Estos cambios se muestran en el siguiente esquema. Coloque el texto nuevo donde se indica en package.json. Los marcadores de posición “…” indican las partes existentes del archivo que no deben modificarse.

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

Abra el proyecto en el IDE de Java de preferencia. (En Eclipse, seleccione Archivo > Importar > Proyectos de Maven existentes).

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

Abra src\StateMachine.sln en Visual Studio.

Haga clic con el botón derecho en el Explorador de soluciones y elija Agregar > Nuevo proyecto. Busque MSTest C# y agregue un Proyecto de prueba con MSTest en C#. (El nombre predeterminado TestProject1 está bien).

Haga clic con el botón derecho en TestProject1 y seleccione Agregar > Referencia del proyecto y agregue el proyecto StateMachine como referencia.

La pila de ejemplo

Esta es la pila que se pondrá a prueba en este tema. Como se describió anteriormente, contiene una función de Lambda y una máquina de estado de Step Functions, y acepta uno o más temas de Amazon SNS. La función de Lambda está suscrita a los temas de Amazon SNS y los reenvía a la máquina de estado.

No tiene que hacer nada en especial para que la aplicación se pueda probar. De hecho, esta pila de CDK no difiere en ningún aspecto importante de las demás pilas de ejemplo de esta guía.

TypeScript

Coloque el siguiente código en 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

Coloque el siguiente código en 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

Coloque el siguiente código en 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); } } } }

Modificaremos el punto de entrada de la aplicación principal para no crear una instancia de nuestra pila. No queremos implementarla por accidente. Nuestras pruebas crearán una aplicación y una instancia de la pila para llevar a cabo las pruebas. Se trata de una táctica útil cuando se combina con el desarrollo basado en pruebas. Asegúrese de que la pila pase todas las pruebas antes de habilitar la implementación.

TypeScript

En 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

En 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

En 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 función de Lambda

Nuestra pila de ejemplo incluye una función de Lambda que inicia nuestra máquina de estado. Debemos proporcionar el código fuente de esta función para que el CDK pueda agruparla e implementarla como parte de la creación del recurso de la función de Lambda.

  • Cree la carpeta start-state-machine en el directorio principal de la aplicación.

  • En esta carpeta, cree al menos un archivo. Por ejemplo, puede guardar el siguiente código en start-state-machines/index.js.

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

    Sin embargo, cualquier archivo funcionará, ya que en realidad no implementaremos la pila.

Ejecución de pruebas

Como referencia, estos son los comandos que utiliza para ejecutar las pruebas en su aplicación de AWS CDK. Estos son los mismos comandos que utilizaría para ejecutar las pruebas en cualquier proyecto en el que se utilice el mismo marco de prueba. En el caso de los lenguajes que requieren un paso de compilación, inclúyalo para asegurarse de que las pruebas se hayan compilado.

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

Cree su solución (F6) para descubrir las pruebas y luego, ejecútelas (Pruebas > Ejecutar todas las pruebas). Para elegir qué pruebas ejecutar, abra el Explorador de pruebas (Pruebas > Explorador de pruebas).

O bien:

dotnet test src

Aserciones detalladas

El primer paso para probar una pila con aserciones detalladas es sintetizar la pila, ya que estamos escribiendo las aserciones en función de la plantilla de AWS CloudFormation generada.

Nuestra StateMachineStackStack requiere que se apruebe el tema de Amazon SNS para que se reenvíe a la máquina de estado. Por eso, en nuestra prueba, crearemos una pila independiente que contenga el tema.

Por lo general, cuando se escribe una aplicación de CDK, se puede subclasificar Stack y crear una instancia del tema de Amazon SNS en el constructor de la pila. En nuestra prueba, creamos una instancia de Stack directamente, luego pasamos esta pila como el ámbito de Topic y la adjuntamos a la pila. Esto es funcionalmente equivalente y menos detallado. Además, ayuda a que las pilas que se utilizan solo en las pruebas tengan un aspecto diferente al de las pilas que se implementarán.

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

Ahora podemos confirmar que la función de Lambda y la suscripción de Amazon SNS se crearon.

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

La prueba de nuestra función de Lambda confirma que dos propiedades particulares del recurso de la función tienen valores específicos. De forma predeterminada, el método hasResourceProperties realiza una coincidencia parcial de las propiedades del recurso, tal como se indica en la plantilla sintetizada de CloudFormation. Esta prueba requiere que las propiedades proporcionadas existan y tengan los valores especificados, pero el recurso también puede tener otras propiedades que no se prueban.

Nuestra aserción de Amazon SNS confirma que la plantilla sintetizada contiene una suscripción, pero no indica nada sobre la suscripción en sí. Incluimos esta aserción principalmente para ilustrar cómo hacer aserciones sobre el recuento de recursos. La clase Template ofrece métodos más específicos para escribir aserciones en las secciones Resources, Outputs y Mapping de la plantilla de CloudFormation.

Comparadores

El comportamiento de coincidencia parcial predeterminado de hasResourceProperties se puede cambiar mediante comparadores de la clase Match.

Los comparadores pueden ser tolerantes (Match.anyValue) o estrictos (Match.objectEquals). Se pueden anidar para aplicar diferentes métodos de coincidencia a diferentes partes de las propiedades del recurso. Si se utilizan Match.objectEquals y Match.anyValue juntos, por ejemplo, se puede probar más exhaustivamente el rol de IAM de la máquina de estado, sin requerir valores específicos para las propiedades que pueden cambiar.

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

Muchos recursos de CloudFormation incluyen objetos JSON serializados representados como cadenas. El comparador Match.serializedJson() se puede utilizar para hacer coincidir las propiedades de este objeto JSON.

Por ejemplo, las máquinas de estado de Step Functions se definen mediante una cadena en el lenguaje Amazon States Language basado en JSON. Utilizaremos Match.serializedJson() para asegurarnos de que nuestro estado inicial sea el único paso. Nuevamente, utilizaremos comparadores anidados para aplicar diferentes tipos de coincidencias a diferentes partes del objeto.

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

Captura

A veces, es útil probar las propiedades para asegurarse de que siguen formatos específicos o que tienen el mismo valor que otra propiedad, sin necesidad de conocer los valores exactos con antelación. El módulo assertions proporciona esta capacidad en su clase Capture.

Cuando especifica una instancia Capture en lugar de un valor en hasResourceProperties, ese valor se retiene en el objeto Capture. El valor real capturado se puede recuperar mediante los métodos as del objeto, incluidos asNumber(), asString() y asObject, y someterse a pruebas. Utilice Capture con un comparador para especificar la ubicación exacta del valor que se capturará entre las propiedades del recurso, incluidas las propiedades JSON serializadas.

El siguiente ejemplo comprueba que el estado inicial de nuestra máquina de estado tenga un nombre que comience por Start. Además, comprueba que este estado esté presente en la lista de estados de la máquina.

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

Pruebas de instantáneas

En las pruebas de instantáneas, se compara toda la plantilla sintetizada de CloudFormation con una plantilla de referencia previamente almacenada (a menudo llamada “maestra”). A diferencia de las aserciones detalladas, las pruebas de instantáneas no son útiles para detectar regresiones. Esto se debe a que las pruebas de instantáneas se aplican a toda la plantilla y, además de cambios en el código, pueden provocar diferencias pequeñas (o no tan pequeñas) en los resultados de la síntesis. Es probable que estos cambios ni siquiera afecten a la implementación, pero aun así provocarán que una prueba de instantáneas falle.

Por ejemplo, puede actualizar un constructo de CDK para incorporar una práctica recomendada nueva, lo que puede provocar cambios en los recursos sintetizados o en la forma en que están organizados. Otra alternativa es actualizar el kit de herramientas de CDK a una versión que incluya metadatos adicionales. Los cambios en los valores de contexto también pueden afectar a la plantilla sintetizada.

Sin embargo, las pruebas de instantáneas pueden ser de gran ayuda a la hora de refactorizar, siempre que se mantengan constantes todos los demás factores que puedan afectar a la plantilla sintetizada. Sabrá de inmediato si un cambio realizado modificó involuntariamente la plantilla. Si el cambio es intencional, simplemente acepte la plantilla nueva como referencia.

Por ejemplo, si tenemos este constructo DeadLetterQueue:

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

Podemos probarlo de la siguiente manera:

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

Consejos para realizar las pruebas

Recuerde que las pruebas durarán tanto como el código que prueban, y se leerán y modificarán con la misma frecuencia. Por lo tanto, vale la pena tomarse un momento para considerar la mejor manera de escribirlas.

No copie ni pegue líneas de configuración ni aserciones comunes. En su lugar, refactorice esta lógica en correcciones o funciones auxiliares. Utilice buenos nombres que reflejen lo que realmente evalúa cada prueba.

No intente hacer demasiado en una sola prueba. Una prueba debe preferiblemente evaluar un solo comportamiento. Si accidentalmente se rompe ese comportamiento, la prueba debería fallar y el nombre de la prueba debería indicar qué es lo que falló. Se trata más bien de un ideal al que aspirar; sin embargo, a veces, de manera inevitable o inadvertida, se escribirán pruebas que evalúen más de un comportamiento. Las pruebas de instantáneas son, por las razones que ya describimos, especialmente propensas a este problema, así que utilícelas con moderación.