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.
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
StateMachineStackStack
El 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 Resources
Outputs
, y de la CloudFormation plantilla.
Comparadores
El comportamiento de coincidencia parcial predeterminado de se hasResourceProperties
puede cambiar utilizando comparadores de la Match
clase.
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 Capture
clase.
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. Capture
Utilí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.