AWS CDK 애플리케이션 테스트 - AWS Cloud Development Kit (AWS CDK) v2

이는 AWS CDK v2 개발자 안내서입니다. 이전 CDK v1은 2022년 6월 1일에 유지 관리에 들어갔으며 2023년 6월 1일에 지원이 종료되었습니다.

AWS CDK 애플리케이션 테스트

AWS CDK를 사용하면 인프라가 작성하는 다른 코드만큼 테스트할 수 있습니다. AWS CDK 앱을 테스트하는 표준 접근 방식은 AWS CDK의 어설션 모듈과 TypeScript 및 JavaScript용 Jest, Python용 Pytest와 같은 인기 테스트 프레임워크를 사용합니다.

AWS CDK 앱용으로 작성할 수 있는 테스트에는 두 가지 범주가 있습니다.

  • 세분화된 어설션은 생성된 AWS CloudFormation 템플릿의 특정 측면을 테스트합니다(예: '이 리소스에는 이 값이 있는 이 속성이 있음'). 이러한 테스트는 회귀를 탐지할 수 있습니다. 또한 테스트 기반 개발을 사용하여 새 기능을 개발할 때도 유용합니다. (테스트를 먼저 작성한 다음 올바른 구현을 작성하여 통과할 수 있습니다.) 세분화된 어설션은 가장 자주 사용되는 테스트입니다.

  • 스냅샷 테스트는 이전에 저장된 기준 템플릿과 비교하여 합성된 AWS CloudFormation 템플릿을 테스트합니다. 스냅샷 테스트를 통해 리팩터링된 코드가 원본과 정확히 동일한 방식으로 작동하는지 확인할 수 있으므로 자유롭게 리팩터링할 수 있습니다. 의도적으로 변경한 경우 향후 테스트에 사용할 새 기준선을 적용할 수 있습니다. 그러나 CDK 업그레이드로 인해 합성된 템플릿도 변경될 수 있으므로 구현이 올바른지 확인하기 위해 스냅샷에만 의존할 수는 없습니다.

참고

이 주제의 예로 사용되는 TypeScript , Python 및 Java 앱의 전체 버전은 GitHub에서 확인할 수 있습니다.

시작하기

이러한 테스트를 작성하는 방법을 설명하기 위해 AWS Step Functions 상태 시스템과 AWS Lambda 함수가 포함된 스택을 생성합니다. Lambda 함수는 Amazon SNS 주제를 구독하고 메시지를 상태 시스템에 전달하기만 하면 됩니다.

먼저 CDK Toolkit를 사용하여 빈 CDK 애플리케이션 프로젝트를 생성하고 필요한 라이브러리를 설치합니다. 여기에서 사용할 구문은 모두 기본 CDK 패키지에 들어 있습니다. 이 패키지는 CDK Toolkit로 생성된 프로젝트의 기본 종속성입니다. 하지만 테스트 프레임워크를 설치해야 합니다.

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

테스트용 디렉터리를 생성합니다.

mkdir test

프로젝트의 package.json을 편집하여 NPM에 Jest를 실행하는 방법을 알리고 Jest에 수집할 파일 유형을 알려줍니다. 필요한 변경 사항은 다음과 같습니다.

  • scripts 섹션에 새 test 키 추가

  • devDependencies 섹션에 Jest 및 해당 유형 추가

  • moduleFileExtensions 선언과 함께 새 jest 최상위 키 추가

이러한 변경 사항은 다음 개요에 나와 있습니다. package.json에 표시된 위치에 새 텍스트를 배치합니다. ’...’ 자리 표시자는 변경해서는 안 되는 파일의 기존 부분을 나타냅니다.

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

테스트용 디렉터리를 생성합니다.

mkdir test

프로젝트의 package.json을 편집하여 NPM에 Jest를 실행하는 방법을 알리고 Jest에 수집할 파일 유형을 알려줍니다. 필요한 변경 사항은 다음과 같습니다.

  • scripts 섹션에 새 test 키 추가

  • devDependencies 섹션에 Jest 추가

  • moduleFileExtensions 선언과 함께 새 jest 최상위 키 추가

이러한 변경 사항은 다음 개요에 나와 있습니다. package.json에 표시된 위치에 새 텍스트를 배치합니다. ’...’ 자리 표시자는 변경해서는 안 되는 파일의 기존 부분을 나타냅니다.

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

원하는 Java IDE에서 프로젝트를 엽니다. Eclipse에서 파일 > 가져오기 > 기존 Maven 프로젝트를 사용합니다.

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

Visual Studio에서 src\StateMachine.sln을 엽니다.

Solution Explorer에서 솔루션을 마우스 오른쪽 버튼으로 클릭하고 추가 > 새 프로젝트 를 선택합니다. MSTest C#을 검색하고 C#에 대한 MSTest Test Project를 추가합니다. 기본 이름은 TestProject1입니다.

TestProject1을 마우스 오른쪽 버튼을 클릭하고 추가 > 프로젝트 참조 를 선택하고 StateMachine 프로젝트를 참조로 추가합니다.

예제 스택

다음은 이 주제에서 테스트할 스택입니다. 앞서 설명한 것처럼 Lambda 함수와 Step Functions 상태 머신이 포함되어 있으며 하나 이상의 Amazon SNS 주제를 수락합니다. Lambda 함수는 Amazon SNS 주제를 구독하고 상태 시스템에 전달합니다.

앱을 테스트하기 위해 특별한 작업을 수행할 필요는 없습니다. 실제로 이 CDK 스택은 이 가이드의 다른 예제 스택과 어떤 중요한 방식으로도 다르지 않습니다.

TypeScript

lib/state-machine-stack.ts에 다음 코드를 넣습니다.

import * as cdk from "aws-cdk-lib"; import * as sns from "aws-cdk-lib/aws-sns"; import * as sns_subscriptions from "aws-cdk-lib/aws-sns-subscriptions"; import * as lambda from "aws-cdk-lib/aws-lambda"; import * as sfn from "aws-cdk-lib/aws-stepfunctions"; import { Construct } from "constructs"; export interface StateMachineStackProps extends cdk.StackProps { readonly topics: sns.Topic[]; } export class StateMachineStack extends cdk.Stack { constructor(scope: Construct, id: string, props: StateMachineStackProps) { super(scope, id, props); // In the future this state machine will do some work... const stateMachine = new sfn.StateMachine(this, "StateMachine", { definition: new sfn.Pass(this, "StartState"), }); // This Lambda function starts the state machine. const func = new lambda.Function(this, "LambdaFunction", { runtime: lambda.Runtime.NODEJS_18_X, handler: "handler", code: lambda.Code.fromAsset("./start-state-machine"), environment: { STATE_MACHINE_ARN: stateMachine.stateMachineArn, }, }); stateMachine.grantStartExecution(func); const subscription = new sns_subscriptions.LambdaSubscription(func); for (const topic of props.topics) { topic.addSubscription(subscription); } } }
JavaScript

lib/state-machine-stack.js에 다음 코드를 넣습니다.

const cdk = require("aws-cdk-lib"); const sns = require("aws-cdk-lib/aws-sns"); const sns_subscriptions = require("aws-cdk-lib/aws-sns-subscriptions"); const lambda = require("aws-cdk-lib/aws-lambda"); const sfn = require("aws-cdk-lib/aws-stepfunctions"); class StateMachineStack extends cdk.Stack { constructor(scope, id, props) { super(scope, id, props); // In the future this state machine will do some work... const stateMachine = new sfn.StateMachine(this, "StateMachine", { definition: new sfn.Pass(this, "StartState"), }); // This Lambda function starts the state machine. const func = new lambda.Function(this, "LambdaFunction", { runtime: lambda.Runtime.NODEJS_18_X, handler: "handler", code: lambda.Code.fromAsset("./start-state-machine"), environment: { STATE_MACHINE_ARN: stateMachine.stateMachineArn, }, }); stateMachine.grantStartExecution(func); const subscription = new sns_subscriptions.LambdaSubscription(func); for (const topic of props.topics) { topic.addSubscription(subscription); } } } module.exports = { StateMachineStack }
Python

state_machine/state_machine_stack.py에 다음 코드를 넣습니다.

from typing import List import aws_cdk.aws_lambda as lambda_ import aws_cdk.aws_sns as sns import aws_cdk.aws_sns_subscriptions as sns_subscriptions import aws_cdk.aws_stepfunctions as sfn import aws_cdk as cdk class StateMachineStack(cdk.Stack): def __init__( self, scope: cdk.Construct, construct_id: str, *, topics: List[sns.Topic], **kwargs ) -> None: super().__init__(scope, construct_id, **kwargs) # In the future this state machine will do some work... state_machine = sfn.StateMachine( self, "StateMachine", definition=sfn.Pass(self, "StartState") ) # This Lambda function starts the state machine. func = lambda_.Function( self, "LambdaFunction", runtime=lambda_.Runtime.NODEJS_18_X, handler="handler", code=lambda_.Code.from_asset("./start-state-machine"), environment={ "STATE_MACHINE_ARN": state_machine.state_machine_arn, }, ) state_machine.grant_start_execution(func) subscription = sns_subscriptions.LambdaSubscription(func) for topic in topics: topic.add_subscription(subscription)
Java
package software.amazon.samples.awscdkassertionssamples; import software.constructs.Construct; import software.amazon.awscdk.Stack; import software.amazon.awscdk.StackProps; import software.amazon.awscdk.services.lambda.Code; import software.amazon.awscdk.services.lambda.Function; import software.amazon.awscdk.services.lambda.Runtime; import software.amazon.awscdk.services.sns.ITopicSubscription; import software.amazon.awscdk.services.sns.Topic; import software.amazon.awscdk.services.sns.subscriptions.LambdaSubscription; import software.amazon.awscdk.services.stepfunctions.Pass; import software.amazon.awscdk.services.stepfunctions.StateMachine; import java.util.Collections; import java.util.List; public class StateMachineStack extends Stack { public StateMachineStack(final Construct scope, final String id, final List<Topic> topics) { this(scope, id, null, topics); } public StateMachineStack(final Construct scope, final String id, final StackProps props, final List<Topic> topics) { super(scope, id, props); // In the future this state machine will do some work... final StateMachine stateMachine = StateMachine.Builder.create(this, "StateMachine") .definition(new Pass(this, "StartState")) .build(); // This Lambda function starts the state machine. final Function func = Function.Builder.create(this, "LambdaFunction") .runtime(Runtime.NODEJS_18_X) .handler("handler") .code(Code.fromAsset("./start-state-machine")) .environment(Collections.singletonMap("STATE_MACHINE_ARN", stateMachine.getStateMachineArn())) .build(); stateMachine.grantStartExecution(func); final ITopicSubscription subscription = new LambdaSubscription(func); for (final Topic topic : topics) { topic.addSubscription(subscription); } } }
C#
using Amazon.CDK; using Amazon.CDK.AWS.Lambda; using Amazon.CDK.AWS.StepFunctions; using Amazon.CDK.AWS.SNS; using Amazon.CDK.AWS.SNS.Subscriptions; using Constructs; using System.Collections.Generic; namespace AwsCdkAssertionSamples { public class StateMachineStackProps : StackProps { public Topic[] Topics; } public class StateMachineStack : Stack { internal StateMachineStack(Construct scope, string id, StateMachineStackProps props = null) : base(scope, id, props) { // In the future this state machine will do some work... var stateMachine = new StateMachine(this, "StateMachine", new StateMachineProps { Definition = new Pass(this, "StartState") }); // This Lambda function starts the state machine. var func = new Function(this, "LambdaFunction", new FunctionProps { Runtime = Runtime.NODEJS_18_X, Handler = "handler", Code = Code.FromAsset("./start-state-machine"), Environment = new Dictionary<string, string> { { "STATE_MACHINE_ARN", stateMachine.StateMachineArn } } }); stateMachine.GrantStartExecution(func); foreach (Topic topic in props?.Topics ?? new Topic[0]) { var subscription = new LambdaSubscription(func); } } } }

실제로 스택을 인스턴스화하지 않도록 앱의 기본 진입점을 수정합니다. 실수로 배포하고 싶지 않습니다. 테스트는 테스트를 위한 앱과 스택 인스턴스를 생성합니다. 이는 테스트 기반 개발과 결합할 때 유용한 전술입니다. 배포를 활성화하기 전에 스택이 모든 테스트를 통과해야 합니다.

TypeScript

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

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

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

Lambda 함수

예제 스택에는 상태 시스템을 시작하는 Lambda 함수가 포함되어 있습니다. CDK가 Lambda 함수 리소스 생성의 일부로 번들링하고 배포할 수 있도록 이 함수의 소스 코드를 제공해야 합니다.

  • 앱의 기본 디렉터리에 start-state-machine 폴더를 생성합니다.

  • 이 폴더에서 파일을 하나 이상 생성합니다. 예를 들어, 다음 코드를 start-state-machines/index.js에 저장할 수 있습니다.

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

    하지만 실제로 스택을 배포하지 않으므로 모든 파일이 작동합니다.

테스트 실행

다음은 AWS CDK 앱에서 테스트를 실행하는 데 사용하는 명령입니다. 이는 동일한 테스트 프레임워크를 사용하여 모든 프로젝트에서 테스트를 실행하는 데 사용하는 것과 동일한 명령입니다. 빌드 단계가 필요한 언어의 경우 테스트가 컴파일되었는지 확인하기 위해 를 포함합니다.

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

솔루션(F6)을 빌드하여 테스트를 검색한 다음 테스트를 실행합니다(테스트 > 모든 테스트 실행 ). 실행할 테스트를 선택하려면 테스트 탐색기(테스트 > 테스트 탐색기)를 엽니다.

또는 다음과 같습니다.

dotnet test src

세분화된 어설션

세분화된 어설션으로 스택을 테스트하는 첫 번째 단계는 생성된 AWS CloudFormation 템플릿에 대해 어설션을 작성하기 때문에 스택을 합성하는 것입니다.

StateMachineStackStack에서는 상태 시스템에 전달하기 위해 Amazon SNS 주제를 전달해야 합니다. 따라서 테스트에서는 주제를 포함하는 별도의 스택을 생성합니다.

일반적으로 CDK 앱을 작성할 때 Stack을 서브클래싱하고 스택의 생성자에서 Amazon SNS 토픽을 인스턴스화할 수 있습니다. 테스트에서는 Stack을 직접 인스턴스화한 다음, 이 스택을 Topic의 범위로 전달하여 스택에 연결합니다. 이는 기능적으로 동일하며 덜 상세합니다. 또한 테스트에만 사용되는 스택을 배포하려는 스택과 '다른 모습'으로 만드는 데도 도움이 됩니다.

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

이제 Lambda 함수와 Amazon SNS 구독이 생성되었다고 주장할 수 있습니다.

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

Lambda 함수 테스트는 함수 리소스의 두 가지 특정 속성에 특정 값이 있다고 주장합니다. 기본적으로 hasResourceProperties 메서드는 합성된 CloudFormation 템플릿에 지정된 대로 리소스 속성에 대해 부분 일치를 수행합니다. 이 테스트를 수행하려면 제공된 속성이 존재하고 지정된 값이 있어야 하지만 리소스에는 테스트되지 않은 다른 속성도 있을 수 있습니다.

Amazon SNS 어설션은 합성된 템플릿에 구독이 포함되어 있지만 구독 자체에 대한 내용은 없다고 주장합니다. 주로 리소스 수를 기준으로 를 어설션하는 방법을 설명하기 위해 이 어설션을 포함했습니다. Template 클래스는 CloudFormation 템플릿의 Resources, OutputsMapping 섹션에 대해 어설션을 작성하는 보다 구체적인 방법을 제공합니다.

매처

hasResourceProperties의 기본 부분 일치 동작은 Match 클래스의 매처를 사용하여 변경할 수 있습니다.

매처의 범위는 관대(Match.anyValue)에서 엄격(Match.objectEquals)까지입니다. 서로 다른 매칭 방법을 리소스 속성의 서로 다른 부분에 적용하도록 중첩할 수 있습니다. 예를 들어 Match.objectEqualsMatch.anyValue를 함께 사용하면 상태 시스템의 IAM 역할을 더 완전하게 테스트할 수 있지만 변경될 수 있는 속성에는 특정 값이 필요하지 않습니다.

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

많은 CloudFormation 리소스에는 문자열로 표시되는 직렬화된 JSON 객체가 포함됩니다. Match.serializedJson() 매처를 사용하여 이 JSON 내의 속성을 일치시킬 수 있습니다.

예를 들어 Step Functions 상태 머신은 JSON 기반 Amazon States Language의 문자열을 사용하여 정의됩니다. Match.serializedJson()을 사용하여 초기 상태가 유일한 단계인지 확인할 것입니다. 다시 말하지만 중첩된 매처를 사용하여 객체의 서로 다른 부분에 서로 다른 종류의 매칭을 적용할 것입니다.

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

캡처

속성을 테스트하여 정확한 값을 미리 알 필요 없이 특정 형식을 따르거나 다른 속성과 동일한 값을 갖도록 하는 것이 유용한 경우가 많습니다. assertions 모듈은 Capture 클래스에서 이 기능을 제공합니다.

hasResourceProperties의 값 대신 Capture 인스턴스를 지정하면 해당 값이 Capture 객체에 유지됩니다. 실제로 캡처된 값은 asNumber(), asString(), asObject를 비롯한 객체의 as 메서드를 사용하여 검색하고 테스트를 거칠 수 있습니다. 매처와 함께 Capture를 사용하여 직렬화된 JSON 속성을 포함하여 리소스 속성 내에서 캡처할 값의 정확한 위치를 지정합니다.

다음 예에서는 상태 시스템의 시작 상태에 Start로 시작하는 이름이 있는지 테스트합니다. 또한 이 상태가 시스템의 상태 목록 내에 있는지 테스트합니다.

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

스냅샷 테스트

스냅샷 테스트에서는 합성된 전체 CloudFormation 템플릿을 이전에 저장된 기준(종종 ’마스터’라고 함) 템플릿과 비교합니다. 세분화된 어설션과 달리 스냅샷 테스트는 회귀를 포착하는 데 유용하지 않습니다. 이는 스냅샷 테스트가 전체 템플릿에 적용되고 코드 변경 이외의 사항으로 인해 합성 결과에 작은(또는 작지 않은) 차이가 발생할 수 있기 때문입니다. 이러한 변경 사항은 배포에도 영향을 미치지 않을 수 있지만 스냅샷 테스트가 실패합니다.

예를 들어 CDK 구문을 업데이트하여 새로운 모범 사례를 통합하면 합성된 리소스 또는 리소스 구성 방식이 변경될 수 있습니다. 또는 CDK Toolkit를 추가 메타데이터를 보고하는 버전으로 업데이트할 수 있습니다. 컨텍스트 값을 변경하면 합성된 템플릿에도 영향을 미칠 수 있습니다.

스냅샷 테스트는 합성된 템플릿에 영향을 미칠 수 있는 다른 모든 요인을 일정하게 유지하는 한 리팩터링에 큰 도움이 될 수 있습니다. 변경 사항이 의도하지 않게 템플릿을 변경한 경우 즉시 알 수 있습니다. 변경 사항이 의도적인 경우 새 템플릿을 기준으로 수락하면 됩니다.

예를 들어 다음과 같은 DeadLetterQueue 구문이 있는 경우

TypeScript
export class DeadLetterQueue extends sqs.Queue { public readonly messagesInQueueAlarm: cloudwatch.IAlarm; constructor(scope: Construct, id: string) { super(scope, id); // Add the alarm this.messagesInQueueAlarm = new cloudwatch.Alarm(this, 'Alarm', { alarmDescription: 'There are messages in the Dead Letter Queue', evaluationPeriods: 1, threshold: 1, metric: this.metricApproximateNumberOfMessagesVisible(), }); } }
JavaScript
class DeadLetterQueue extends sqs.Queue { constructor(scope, id) { super(scope, id); // Add the alarm this.messagesInQueueAlarm = new cloudwatch.Alarm(this, 'Alarm', { alarmDescription: 'There are messages in the Dead Letter Queue', evaluationPeriods: 1, threshold: 1, metric: this.metricApproximateNumberOfMessagesVisible(), }); } } module.exports = { DeadLetterQueue }
Python
class DeadLetterQueue(sqs.Queue): def __init__(self, scope: Construct, id: str): super().__init__(scope, id) self.messages_in_queue_alarm = cloudwatch.Alarm( self, "Alarm", alarm_description="There are messages in the Dead Letter Queue.", evaluation_periods=1, threshold=1, metric=self.metric_approximate_number_of_messages_visible(), )
Java
public class DeadLetterQueue extends Queue { private final IAlarm messagesInQueueAlarm; public DeadLetterQueue(@NotNull Construct scope, @NotNull String id) { super(scope, id); this.messagesInQueueAlarm = Alarm.Builder.create(this, "Alarm") .alarmDescription("There are messages in the Dead Letter Queue.") .evaluationPeriods(1) .threshold(1) .metric(this.metricApproximateNumberOfMessagesVisible()) .build(); } public IAlarm getMessagesInQueueAlarm() { return messagesInQueueAlarm; } }
C#
namespace AwsCdkAssertionSamples { public class DeadLetterQueue : Queue { public IAlarm messagesInQueueAlarm; public DeadLetterQueue(Construct scope, string id) : base(scope, id) { messagesInQueueAlarm = new Alarm(this, "Alarm", new AlarmProps { AlarmDescription = "There are messages in the Dead Letter Queue.", EvaluationPeriods = 1, Threshold = 1, Metric = this.MetricApproximateNumberOfMessagesVisible() }); } } }

다음과 같이 테스트할 수 있습니다.

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

테스트 팁

테스트는 테스트하는 코드만큼 오래 지속되며, 자주 읽고 수정됩니다. 따라서 잠시 시간을 내어 가장 잘 쓰는 방법을 고려하는 것이 좋습니다.

설정 라인이나 일반적인 어설션을 복사하여 붙여넣지 마세요. 대신 이 로직을 픽스처 또는 도우미 함수로 리팩터링합니다. 각 테스트가 실제로 테스트한 내용을 반영하는 이름을 사용합니다.

한 번의 테스트로 너무 많이 시도하지 마세요. 가급적이면 테스트는 하나만 테스트해야 합니다. 실수로 해당 동작을 중단하면 정확히 하나의 테스트가 실패하고 테스트 이름이 실패한 항목을 알려주어야 합니다. 그러나 이는 노력하는 데 더 이상 좋습니다. 때로는 부득이하게(또는 실수로) 하나 이상의 동작을 테스트하는 테스트를 작성합니다. 스냅샷 테스트는 이미 설명한 이유로 특히 이 문제가 발생하기 쉬우므로 조금씩 사용합니다.