AWS CDK Aplicativos de teste - AWS Cloud Development Kit (AWS CDK) v2

Este é o Guia do Desenvolvedor AWS CDK v2. A versão CDK 1 mais antiga entrou em manutenção em 1º de junho de 2022 e encerrou o suporte em 1º de junho de 2023.

As traduções são geradas por tradução automática. Em caso de conflito entre o conteúdo da tradução e da versão original em inglês, a versão em inglês prevalecerá.

AWS CDK Aplicativos de teste

Com o AWS CDK, sua infraestrutura pode ser tão testável quanto qualquer outro código que você escrever. A abordagem padrão para testar AWS CDK aplicativos usa o módulo AWS CDK de asserções do e estruturas de teste populares, como Jest for TypeScript and JavaScript ou Pytest for Python.

Há duas categorias de testes que você pode escrever para AWS CDK aplicativos.

  • Asserções refinadas testam aspectos específicos do AWS CloudFormation modelo gerado, como “esse recurso tem essa propriedade com esse valor”. Esses testes podem detectar regressões. Eles também são úteis quando você desenvolve novos recursos usando o desenvolvimento orientado a testes. (Você pode escrever um teste primeiro e depois fazê-lo passar escrevendo uma implementação correta.) Afirmações refinadas são os testes usados com mais frequência.

  • Os testes instantâneos testam o modelo sintetizado em relação a um AWS CloudFormation modelo de linha de base armazenado anteriormente. Os testes instantâneos permitem que você refatore livremente, pois você pode ter certeza de que o código refatorado funciona exatamente da mesma forma que o original. Se as alterações foram intencionais, é possível aceitar uma nova linha de base para futuros testes. No entanto, CDK as atualizações também podem fazer com que os modelos sintetizados sejam alterados, portanto, você não pode confiar apenas em instantâneos para garantir que sua implementação esteja correta.

nota

Versões completas dos TypeScript aplicativos Python e Java usados como exemplos neste tópico estão disponíveis em. GitHub

Conceitos básicos

Para ilustrar como escrever esses testes, criaremos uma pilha que contém uma máquina de AWS Step Functions estados e uma AWS Lambda função. A função Lambda está inscrita em um SNS tópico da Amazon e simplesmente encaminha a mensagem para a máquina de estado.

Primeiro, crie um projeto de CDK aplicativo vazio usando o CDK Toolkit e instalando as bibliotecas de que precisaremos. As construções que usaremos estão todas no CDK pacote principal, que é uma dependência padrão em projetos criados com o CDK Toolkit. No entanto, você deve instalar sua estrutura de teste.

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

Crie um diretório para seus testes.

mkdir test

Edite o projeto package.json para saber NPM como executar o Jest e para dizer ao Jest quais tipos de arquivos coletar. As mudanças necessárias são as seguintes.

  • Adicione uma nova test chave à scripts seção

  • Adicione Jest e seus tipos à seção devDependencies

  • Adicione uma nova chave jest de nível superior com uma declaração moduleFileExtensions

Essas mudanças são mostradas no esboço a seguir. Coloque o novo texto onde indicado empackage.json. Os espaços reservados “...” indicam partes existentes do arquivo que não devem ser alteradas.

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

Crie um diretório para seus testes.

mkdir test

Edite o projeto package.json para saber NPM como executar o Jest e para dizer ao Jest quais tipos de arquivos coletar. As mudanças necessárias são as seguintes.

  • Adicione uma nova test chave à scripts seção

  • Adicione Jest à seção devDependencies

  • Adicione uma nova chave jest de nível superior com uma declaração moduleFileExtensions

Essas mudanças são mostradas no esboço a seguir. Coloque o novo texto onde indicado empackage.json. Os espaços reservados “...” indicam partes existentes do arquivo que não devem ser alteradas.

{ ... "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 o projeto em seu Java preferidoIDE. (No Eclipse, use Arquivo > Importar > Projetos existentes do Maven.)

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

Abra a src\StateMachine.sln no Visual Studio.

Clique com o botão direito do mouse na solução no Solution Explorer e escolha Adicionar > Novo projeto. Pesquise MSTest C# e adicione um projeto de MSTest teste para C#. (O nome padrão TestProject1 é bom.)

Clique com o botão direito do mouse TestProject1 e escolha Adicionar > Referência do projeto e adicione o StateMachine projeto como referência.

A pilha de exemplos

Aqui está a pilha que será testada neste tópico. Como descrevemos anteriormente, ele contém uma função Lambda e uma máquina de estado Step Functions e aceita um ou mais tópicos da AmazonSNS. A função Lambda é inscrita nos SNS tópicos da Amazon e os encaminha para a máquina de estado.

Você não precisa fazer nada de especial para tornar o aplicativo testável. Na verdade, essa CDK pilha não é diferente em nada importante das outras pilhas de exemplo neste Guia.

TypeScript

Insira o seguinte código emlib/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

Insira o seguinte código emlib/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

Insira o seguinte código emstate_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 o ponto de entrada principal do aplicativo para que não instanciemos realmente nossa pilha. Não queremos implantá-lo acidentalmente. Nossos testes criarão um aplicativo e uma instância da pilha para testes. Essa é uma tática útil quando combinada com o desenvolvimento orientado a testes: certifique-se de que a pilha passe em todos os testes antes de ativar a implantação.

TypeScript

Em 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

Em 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

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

A função Lambda

Nossa pilha de exemplos inclui uma função Lambda que inicia nossa máquina de estado. Devemos fornecer o código-fonte dessa função para que eles CDK possam agrupá-la e implantá-la como parte da criação do recurso da função Lambda.

  • Crie a pasta start-state-machine no diretório principal do aplicativo.

  • Nessa pasta, crie pelo menos um arquivo. Por exemplo, você pode salvar o código a seguir emstart-state-machines/index.js.

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

    No entanto, qualquer arquivo funcionará, já que não vamos realmente implantar a pilha.

Execução de testes

Para referência, aqui estão os comandos que você usa para executar testes no seu AWS CDK aplicativo. Esses são os mesmos comandos que você usaria para executar os testes em qualquer projeto usando a mesma estrutura de teste. Para linguagens que exigem uma etapa de construção, inclua-a para garantir que seus testes tenham sido compilados.

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

Crie sua solução (F6) para descobrir os testes e, em seguida, execute-os (Teste > Executar todos os testes). Para escolher quais testes executar, abra o Test Explorer (Test > Test Explorer).

Ou:

dotnet test src

Afirmações refinadas

A primeira etapa para testar uma pilha com afirmações refinadas é sintetizar a pilha, porque estamos escrevendo afirmações com base no modelo gerado. AWS CloudFormation

Nossa StateMachineStackStack exigência é que passemos o SNS tópico da Amazon para ser encaminhado à máquina estadual. Então, em nosso teste, criaremos uma pilha separada para conter o tópico.

Normalmente, ao escrever um CDK aplicativo, você pode criar uma subclasse Stack e instanciar o tópico da SNS Amazon no construtor da pilha. Em nosso teste, instanciamos Stack diretamente e, em seguida, passamos essa pilha como escopo, anexando-a à Topic pilha. Isso é funcionalmente equivalente e menos detalhado. Também ajuda a fazer com que as pilhas usadas somente em testes “pareçam diferentes” das pilhas que você pretende implantar.

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

Agora podemos afirmar que a função Lambda e a assinatura da SNS Amazon foram criadas.

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

Nosso teste de função Lambda afirma que duas propriedades específicas do recurso da função têm valores específicos. Por padrão, o hasResourceProperties método executa uma correspondência parcial nas propriedades do recurso, conforme indicado no modelo sintetizado CloudFormation . Esse teste exige que as propriedades fornecidas existam e tenham os valores especificados, mas o recurso também pode ter outras propriedades que não foram testadas.

Nossa SNS afirmação da Amazon afirma que o modelo sintetizado contém uma assinatura, mas nada sobre a assinatura em si. Incluímos essa afirmação principalmente para ilustrar como afirmar a contagem de recursos. A Template classe oferece métodos mais específicos para escrever afirmações nas Mapping seções ResourcesOutputs, e do CloudFormation modelo.

Combinadores

O comportamento padrão de correspondência parcial de hasResourceProperties pode ser alterado usando matchers da Matchclasse.

Os matchers variam de tolerantes (Match.anyValue) a estritos (). Match.objectEquals Eles podem ser agrupados para aplicar diferentes métodos de correspondência a diferentes partes das propriedades do recurso. Usando Match.objectEquals e Match.anyValue juntos, por exemplo, podemos testar a IAM função da máquina de estados de forma mais completa, sem exigir valores específicos para propriedades que possam mudar.

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

Muitos CloudFormation recursos incluem JSON objetos serializados representados como cadeias de caracteres. O Match.serializedJson() matcher pode ser usado para combinar propriedades dentro dele. JSON

Por exemplo, as máquinas de estado do Step Functions são definidas usando uma string na Amazon States Language JSON baseada. Usaremos Match.serializedJson() para garantir que nosso estado inicial seja a única etapa. Novamente, usaremos combinadores aninhados para aplicar diferentes tipos de correspondência a diferentes partes do 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

Geralmente, é útil testar propriedades para garantir que elas sigam formatos específicos ou tenham o mesmo valor de outra propriedade, sem precisar saber seus valores exatos com antecedência. O assertions módulo fornece esse recurso em sua Captureclasse.

Ao especificar uma Capture instância no lugar de um valor emhasResourceProperties, esse valor é retido no Capture objeto. O valor real capturado pode ser recuperado usando os as métodos do objeto, incluindoasNumber(), e asString()asObject, e submetido a testes. Use Capture com um matcher para especificar a localização exata do valor a ser capturado nas propriedades do recurso, incluindo propriedades serializadasJSON.

O exemplo a seguir testa para garantir que o estado inicial de nossa máquina de estado tenha um nome que comece comStart. Também testa se esse estado está presente na lista de estados da 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()));

Testes instantâneos

No teste de instantâneo, você compara todo o modelo sintetizado com um CloudFormation modelo de linha de base armazenado anteriormente (geralmente chamado de “mestre”). Ao contrário das afirmações refinadas, o teste instantâneo não é útil para capturar regressões. Isso ocorre porque o teste de instantâneo se aplica a todo o modelo, e coisas além das alterações no código podem causar pequenas (ou not-so-small) diferenças nos resultados da síntese. Essas alterações podem nem mesmo afetar sua implantação, mas ainda assim farão com que um teste de snapshot falhe.

Por exemplo, você pode atualizar uma CDK construção para incorporar uma nova prática recomendada, que pode causar alterações nos recursos sintetizados ou na forma como eles são organizados. Como alternativa, você pode atualizar o CDK Toolkit para uma versão que relata metadados adicionais. Alterações nos valores de contexto também podem afetar o modelo sintetizado.

No entanto, os testes instantâneos podem ser de grande ajuda na refatoração, desde que você mantenha constantes todos os outros fatores que possam afetar o modelo sintetizado. Você saberá imediatamente se uma alteração que você fez alterou involuntariamente o modelo. Se a alteração for intencional, basta aceitar o novo modelo como linha de base.

Por exemplo, se tivermos essa DeadLetterQueue construção:

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 testá-lo assim:

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

Dicas para testes

Lembre-se de que seus testes durarão tanto quanto o código testado e serão lidos e modificados com a mesma frequência. Portanto, vale a pena considerar a melhor forma de escrevê-las.

Não copie e cole linhas de configuração ou afirmações comuns. Em vez disso, refatore essa lógica em fixtures ou funções auxiliares. Use bons nomes que reflitam o que cada teste realmente testa.

Não tente fazer muita coisa em um teste. De preferência, um teste deve testar um e somente um comportamento. Se você quebrar acidentalmente esse comportamento, exatamente um teste falhará e o nome do teste deverá indicar o que falhou. No entanto, esse é mais um ideal a ser buscado; às vezes, você inevitavelmente (ou inadvertidamente) escreve testes que testam mais de um comportamento. Os testes instantâneos são, por motivos que já descrevemos, especialmente propensos a esse problema, portanto, use-os com moderação.