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.
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 Resources
Outputs
, e do CloudFormation modelo.
Combinadores
O comportamento padrão de correspondência parcial de hasResourceProperties
pode ser alterado usando matchers da Match
classe.
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 Capture
classe.
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.