AWS CDK Aplicaciones de prueba - 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.

AWS CDK Aplicaciones de prueba

Con él AWS CDK, su infraestructura puede ser tan comprobable como cualquier otro código que escriba. El enfoque estándar para probar AWS CDK aplicaciones utiliza el módulo AWS CDK de aserciones y marcos de prueba populares como Jest para TypeScript y JavaScript Pytest para Python.

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

  • Las afirmaciones detalladas prueban aspectos específicos de la AWS CloudFormation plantilla generada, como «este recurso tiene esta propiedad con este valor». Estas pruebas pueden detectar regresiones. También son útiles cuando se desarrollan nuevas funciones mediante el desarrollo basado en pruebas. (Puedes escribir primero una prueba y, después, hacerla pasar escribiendo una implementación correcta). Las aserciones detalladas son las pruebas que se utilizan con más frecuencia.

  • Las pruebas instantáneas comparan la AWS CloudFormation plantilla sintetizada con una plantilla de referencia previamente almacenada. Las pruebas instantáneas le permiten refactorizar libremente, ya que puede estar seguro de que el código refactorizado funciona exactamente de la misma manera que el original. Si los cambios eran intencionales, puede aceptar un punto de referencia nuevo para pruebas futuras. Sin embargo, CDK las actualizaciones también pueden provocar cambios en las plantillas sintetizadas, por lo que no puede confiar únicamente en las instantáneas para asegurarse de que la implementación es correcta.

nota

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

Introducción

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

Primero, crea un proyecto de CDK aplicación vacío con el CDK kit de herramientas e instala las bibliotecas que necesitemos. Las construcciones que usaremos están todas en el CDK paquete principal, que es una dependencia predeterminada en los proyectos creados con el CDK kit de herramientas. Sin embargo, debe instalar su marco de pruebas.

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

Cree un directorio para sus pruebas.

mkdir test

Edita el proyecto package.json para indicarle NPM cómo ejecutar Jest y qué tipos de archivos recopilar. Los cambios necesarios son los siguientes.

  • Añada una nueva test clave a la scripts sección

  • Agrega Jest y sus tipos a la sección devDependencies

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

Estos cambios se muestran en el siguiente esquema. Coloque el nuevo texto donde se indicapackage.json. Los marcadores de posición «...» indican las partes existentes del archivo que no se deben cambiar.

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

mkdir test

Edita el proyecto package.json para indicarle NPM cómo ejecutar Jest y qué tipos de archivos recopilar. Los cambios necesarios son los siguientes.

  • Añada una nueva test clave a la scripts sección

  • Agrega Jest a la sección devDependencies

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

Estos cambios se muestran en el siguiente esquema. Coloque el nuevo texto donde se indicapackage.json. Los marcadores de posición «...» indican las partes existentes del archivo que no se deben cambiar.

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

Abre el proyecto en el Java IDE que prefieras. (En Eclipse, usa Archivo > Importar > Proyectos 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 la solución en el Explorador de soluciones y elija Agregar > Nuevo proyecto. Busque MSTest C# y añada un proyecto de MSTest prueba para C#. (El nombre predeterminado TestProject1 es correcto).

Haga clic con el botón derecho del ratón TestProject1 y seleccione Añadir > Referencia del StateMachine proyecto y añada el proyecto como referencia.

La pila de ejemplos

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

No tiene que hacer nada especial para que la aplicación sea comprobable. De hecho, esta CDK pila no es diferente en ningún aspecto importante de las otras pilas de ejemplo de esta guía.

TypeScript

Coloca el siguiente código enlib/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

Introduce el siguiente código enlib/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

Introduce el siguiente código enstate_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 principal de la aplicación para que en realidad no instanciemos nuestra pila. No queremos desplegarla accidentalmente. Nuestras pruebas crearán una aplicación y una instancia de la pila para probarlas. Se trata de una táctica útil cuando se combina con un desarrollo basado en pruebas: asegúrate de que la pila supere todas las pruebas antes de habilitar el despliegue.

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 Lambda

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

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

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

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

    Sin embargo, cualquier archivo funcionará, ya que en realidad no vamos a implementar la pila.

Ejecución de pruebas

Como referencia, estos son los comandos que usas para ejecutar pruebas en tu AWS CDK aplicación. Estos son los mismos comandos que utilizarías para ejecutar las pruebas en cualquier proyecto que utilice el mismo marco de pruebas. En el caso de los lenguajes que requieren un paso de compilación, inclúyelo para asegurarte 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, a continuación, ejecútelas (Probar > Ejecutar todas las pruebas). Para elegir qué pruebas ejecutar, abra el Explorador de pruebas (Prueba > Explorador de pruebas).

O bien:

dotnet test src

Afirmaciones detalladas

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

StateMachineStackStackEl nuestro requiere que le pasemos el SNS tema de Amazon para que se reenvíe a la máquina de estados. Por eso, en nuestra prueba, crearemos una pila separada para incluir el tema.

Normalmente, al escribir una CDK aplicación, puedes subclasificar Stack e instanciar el tema de SNS Amazon en el constructor de la pila. En nuestra prueba, instanciamos Stack directamente, luego pasamos esta pila como ámbito y la adjuntamos a Topic la pila. Esto es funcionalmente equivalente y menos detallado. También ayuda a que las pilas que se utilizan solo en las pruebas tengan un aspecto diferente al de las pilas que se van a implementar.

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 afirmar que se crearon la función Lambda y la SNS suscripción a Amazon.

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

Nuestra prueba de función Lambda afirma que dos propiedades particulares del recurso de la función tienen valores específicos. De forma predeterminada, el hasResourceProperties método realiza una coincidencia parcial con las propiedades del recurso, tal como se indica en la plantilla sintetizada 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 SNS afirmación de Amazon afirma que la plantilla sintetizada contiene una suscripción, pero nada sobre la suscripción en sí. Incluimos esta afirmación principalmente para ilustrar cómo hacer valer la cantidad de recursos. La Template clase ofrece métodos más específicos para escribir afirmaciones en las Mapping secciones ResourcesOutputs, y de la CloudFormation plantilla.

Comparadores

El comportamiento de coincidencia parcial predeterminado de se hasResourceProperties puede cambiar utilizando comparadores de la Matchclase.

Los comparadores van desde indulgente (Match.anyValue) hasta estricta (). Match.objectEquals Se pueden anidar para aplicar diferentes métodos de coincidencia a diferentes partes de las propiedades del recurso. Al Match.objectEquals Match.anyValue utilizarlos juntos, por ejemplo, podemos probar la IAM función de la máquina de estados de forma más exhaustiva, sin requerir valores específicos para las propiedades que puedan 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 CloudFormation recursos incluyen JSON objetos serializados representados como cadenas. El Match.serializedJson() comparador se puede usar para hacer coincidir las propiedades que contiene. JSON

Por ejemplo, las máquinas de estados Step Functions se definen mediante una cadena en el lenguaje JSON basado en Amazon States Language. Lo usaremos Match.serializedJson() para asegurarnos de que nuestro estado inicial sea el único paso. Nuevamente, usaremos 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() } }} }} }) )}});

Capturando

Suele ser ú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 sus valores exactos con antelación. El assertions módulo proporciona esta capacidad en su Captureclase.

Al especificar una Capture instancia en lugar de un valor enhasResourceProperties, ese valor se conserva en el Capture objeto. El valor capturado real se puede recuperar mediante los as métodos del objeto, incluidos asNumber()asString(), yasObject, y someterlo a pruebas. CaptureUtilícelo con un comparador para especificar la ubicación exacta del valor que se va a capturar dentro de las propiedades del recurso, incluidas las propiedades serializadasJSON.

El siguiente ejemplo comprueba que el estado inicial de nuestra máquina de estados tenga un nombre que comience por. Start También 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 instantáneas

En las pruebas de instantáneas, se compara toda la CloudFormation plantilla sintetizada con una plantilla de referencia previamente almacenada (a menudo denominada «maestra»). A diferencia de las afirmaciones detalladas, las pruebas instantáneas no son útiles para detectar regresiones. Esto se debe a que las pruebas instantáneas se aplican a toda la plantilla y, además de los cambios en el código, pueden provocar pequeñas (o not-so-small) diferencias en los resultados de la síntesis. Es posible 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 una CDK construcción para incorporar una nueva práctica recomendada, lo que puede provocar cambios en los recursos sintetizados o en la forma en que están organizados. Como alternativa, puede actualizar el CDK kit de herramientas 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 instantáneas pueden ser de gran ayuda a la hora de refactorizar, siempre y cuando se mantengan constantes todos los demás factores que puedan afectar a la plantilla sintetizada. Sabrá inmediatamente si un cambio realizado ha modificado la plantilla de forma involuntaria. Si el cambio es intencionado, simplemente acepte la nueva plantilla como referencia.

Por ejemplo, si tenemos esta DeadLetterQueue construcción:

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 así:

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 las pruebas

Recuerde que sus pruebas durarán tanto como el código que prueben, 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 copies ni pegues líneas de configuración ni afirmaciones comunes. En su lugar, refactoriza esta lógica en accesorios o funciones auxiliares. Usa buenos nombres que reflejen lo que realmente evalúa cada prueba.

No intentes hacer demasiado en una sola prueba. Preferiblemente, una prueba debe evaluar una y solo una conducta. Si accidentalmente rompes ese comportamiento, exactamente una prueba debería fallar y el nombre de la prueba debería indicar qué es lo que falló. Sin embargo, lo ideal es esforzarse por lograrlo; a veces, de manera inevitable (o inadvertida), escribirás pruebas que evalúan más de un comportamiento. Las pruebas instantáneas son, por las razones que ya hemos descrito, especialmente propensas a este problema, así que úsalas con moderación.