Tuesday, September 27, 2022
HomeJavaTesting CDK Purposes in Any Language

Testing CDK Purposes in Any Language


The AWS Cloud Growth Package (AWS CDK) is an open supply software program improvement framework to outline your cloud utility sources utilizing acquainted programming languages. As a result of the AWS CDK lets you outline your infrastructure in common programming languages, you too can write automated unit exams to your infrastructure code, similar to you do to your utility code. Testing is a vital ingredient to extremely efficient DevOps practices, and testing your infrastructure code gives advantages equivalent to making certain that you’ll create precisely the sources you anticipate within the AWS cloud and serving to to forestall regressions from being launched to your infrastructure.

At this time, I’m blissful to announce the assertions module for the AWS Cloud Growth Package, a set of APIs designed that will help you write unit exams towards your CDK functions, with a deal with CloudFormation templates.

Cross-Language Help

A earlier AWS weblog put up explains write exams to your infrastructure constructs utilizing the assert module, which is accessible just for JavaScript and TypeScript.

Just like the assert module, the brand new CDK assertions module gives a sturdy set of APIs to exactly confirm the CloudFormation templates synthesized by your CDK app. Moreover, the assertions module is accessible for each language supported by the CDK.

Whereas the brand new assertions module helps each language that’s supported by the CDK, this snippets on this article will likely be written in Python. Nonetheless, the total supply code for these examples is out there on GitHub, and incorporates equal code written in TypeScript, Java, and Python.

If you’re at the moment utilizing the assert module and want to migrate, you will discover a information on that in our GitHub repository.

Nice-grained Assertions

The assertions module gives a number of instruments to each assert that sure elements of a template matches given objects and to retrieve sure elements of of a template. Utilizing these instruments, we are able to assert that sources with a given kind and properties exist, assert that sure outputs exist, and assert {that a} template has a given variety of sources.

Let’s assume you’ve a stack that creates an AWS Step Capabilities state machine and an AWS Lambda perform. The Lambda perform is subscribed to an Amazon SNS matter and easily forwards the message to the state machine:

from typing import Record

from aws_cdk import aws_lambda as lambda_
from aws_cdk import aws_sns as sns
from aws_cdk import aws_sns_subscriptions as sns_subscriptions
from aws_cdk import aws_stepfunctions as sfn
from aws_cdk import core as cdk


class ProcessorStack(cdk.Stack):
    def __init__(
        self,
        scope: cdk.Assemble,
        construct_id: str,
        *,
        matters: Record[sns.Topic],
        **kwargs
    ) -> None:
        tremendous().__init__(scope, construct_id, **kwargs)

        # Sooner or later this state machine will do some work...
        state_machine = sfn.StateMachine(
            self, "StateMachine", definition=sfn.Move(self, "StartState")
        )

        # This Lambda perform begins the state machine.
        func = lambda_.Perform(
            self,
            "LambdaFunction",
            runtime=lambda_.Runtime.NODEJS_14_X,
            handler="handler",
            code=lambda_.Code.from_asset("./start-state-machine"),
            setting={
                "STATE_MACHINE_ARN": state_machine.state_machine_arn,
            },
        )
        state_machine.grant_start_execution(func)

        subscription = sns_subscriptions.LambdaSubscription(func)
        for matter in matters:
            matter.add_subscription(subscription)

How do you take a look at this stack with fine-grained assertions? First, begin by creating the ProcessorStack and synthesizing it right into a CloudFormation template. After getting synthesized your stack right into a Template, you may assert on it. You’ll discover that each take a look at utilizing the assertions module begins with first making a stack and synthesizing it.

Moreover, since this stack depends on cross-stack references, you may create a stack for the referenced sources to dwell in and move these sources to the ProcessorStack:

from aws_cdk import aws_sns as sns
from aws_cdk import core as cdk
from aws_cdk.assertions import Template

from app.processor_stack import ProcessorStack


def test_synthesizes_properly():
    app = cdk.App()

    # Because the ProcessorStack consumes sources from a separate stack
    # (cross-stack references), we create a stack for our SNS matters to dwell
    # in right here. These matters can then be handed to the ProcessorStack later,
    # making a cross-stack reference.
    topics_stack = cdk.Stack(app, "TopicsStack")

    # Create the subject the stack we're testing will reference.
    matters = [sns.Topic(topics_stack, "Topic1")]

    # Create the ProcessorStack.
    processor_stack = ProcessorStack(
        app, "ProcessorStack", matters=matters  # Cross-stack reference
    )

    # Put together the stack for assertions.
    template = Template.from_stack(processor_stack)

Now you may assert that the Lambda perform and a subscription had been created:

    # ...

    # Assert it creates the perform with the right properties...
    template.has_resource_properties(
        "AWS::Lambda::Perform",
        {
            "Handler": "handler",
            "Runtime": "nodejs14.x",
        },
    )

    # Creates the subscription...
    template.resource_count_is("AWS::SNS::Subscription", 1)

    # ...

The has_resource_properties() technique means that you can assert that the template has a useful resource of the given kind with the given properties. This instance asserts that every of the anticipated sources exist and they’re configured with the precise properties outlined within the exams. There are additionally many different strategies on the Template class which can be utilized to confirm the Sources, Outputs, and Mappings sections of the CloudFormation template.

Matchers

Trying on the assertions within the earlier instance, you’ll discover that the examined properties are all outlined as literal values. Nonetheless, the strategies within the assertions module additionally settle for particular matchers which let you outline partial or particular sample matching throughout template assertions. There are a number of matchers constructed into the assertions module, with the total listing documented within the assertions API reference. For instance, the Match.object_like() technique, checks that the anticipated values (the properties handed to has_resource_properties()) are a subset of the goal values (the properties on the synthesized useful resource). The Match.object_like() matcher is often used to forestall exams from failing when further (presumably unrelated) properties are launched by the CDK assemble library.

The subsequent examples present you use varied matchers in your unit exams. The Match.object_equals() matcher checks that the anticipated values are precisely equal to the goal values, somewhat than a subset. However, the Match.any_value() matcher permits the assertion to move it doesn’t matter what the goal worth is. Utilizing a mix of those two matchers, you may totally assert on the state machine’s IAM position:

    from aws_cdk.assertions import Match

    # ...

    # Totally assert on the state machine's IAM position with matchers.
    template.has_resource_properties(
        "AWS::IAM::Position",
        Match.object_equals(
            {
                "AssumeRolePolicyDocument": {
                    "Model": "2012-10-17",
                    "Assertion": [
                        {
                            "Action": "sts:AssumeRole",
                            "Effect": "Allow",
                            "Principal": {
                                "Service": {
                                    "Fn::Join": [
                                        "",
                                        [
                                            "states.",
                                            Match.any_value(),
                                            ".amazonaws.com",
                                        ],
                                    ],
                                },
                            },
                        },
                    ],
                },
            }
        ),
    )

Within the subsequent instance, assertions for the Step Capabilities state machine are included within the unit exams. State machines are outlined by strings written within the Amazon State Languages (based mostly on JSON). Whereas this can be a nice method to specific state machines, it’s troublesome to check with none instruments to assist out. Happily, the assertions module gives a matcher, Match.serialized_json(), which JSON-deserializes the goal string and matches it towards the anticipated worth. You possibly can even nest matchers within a Match.serialized_json() matcher!

The next instance makes use of Match.serialized_json() to say on the state machine’s definition:

    # ...

    # 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 used implicitly, however we use it explicitly
                # right here for further readability.
                Match.object_equals(
                    {
                        "StartAt": "StartState",
                        "States": {
                            "StartState": {
                                "Sort": "Move",
                                "Finish": True,
                                # Ensure that this state would not present a subsequent state --
                                # we will not present each Subsequent and set Finish to true.
                                "Subsequent": Match.absent(),
                            },
                        },
                    }
                )
            ),
        },
    )

Capturing

The Seize API of the assertions module means that you can retrieve values the assertions module encounters when it’s matching and carry out your personal assertions on these values later. You create a Seize object, use it in your assertion simply as if it was another matcher, after which retrieve its worth with the related as_x() technique (for instance, as_string() or as_object()).

This snippet makes use of a mixtures of Captures and the Match.serialized_json() matcher to say that the identify of the beginning state of a state machine begins with “Begin” and that the beginning state is definitely current inside the listing of states within the machine, performing some extra rudimentary validation on the state machine definition:

    import re

    from aws_cdk.assertions import Seize

    # ...

    # Seize some information from the state machine's definition.
    start_at_capture = Seize()
    states_capture = Seize()
    template.has_resource_properties(
        "AWS::StepFunctions::StateMachine",
        {
            "DefinitionString": Match.serialized_json(
                Match.object_like(
                    {
                        "StartAt": start_at_capture,
                        "States": states_capture,
                    }
                )
            ),
        },
    )

    # Assert that the beginning state begins with "Begin".
    assert re.match("^Begin", start_at_capture.as_string())

    # Assert that the beginning state really exists within the states object of the
    # state machine definition.
    assert start_at_capture.as_string() in states_capture.as_object()

Snapshot Testing

One other method to take a look at your CDK functions is utilizing snapshot exams. Snapshot exams take a snapshot of an object the primary time they run. This snapshot is dedicated to model management, and each time the take a look at is run after that, the thing is in comparison with the snapshot. If the snapshot matches the thing, the assertion passes. If the snapshot doesn’t match, the assertion fails.

Snapshot testing, in contrast to customary unit testing, will not be a mechanism to detect regressions. The CloudFormation template produced in the course of the synthesis of an AWS CDK app is influenced by each the CDK utility code in addition to the CDK framework. In some circumstances, the synthesized template can change when the model of the CDK framework is upgraded. This usually occurs when a brand new greatest apply will get included into the CDK. For that reason, snapshot testing is greatest used as a mechanism to warn you when something in any respect adjustments in your CDK stacks. Snapshot testing will make these adjustments seen to you early.

Refactoring your CDK code is one other good use of snapshot testing — you don’t need something to alter when you’re refactoring, and snapshot exams will clearly present you when that occurs. For nearly all different use circumstances, fine-grained assertions are a greater instrument.

You possibly can make use of snapshot testing with the assertions module by first synthesizing the stack right into a CloudFormation template, changing your complete template to an object (in Java, a Map, in Python, a dict), after which utilizing our take a look at framework’s snapshot testing functionalities to say that the template matches that snapshot.

from aws_cdk import aws_sns as sns
from aws_cdk import core as cdk
from aws_cdk.assertions import Template

from app.processor_stack import ProcessorStack


# The snapshot parameter is injected by Pytest -- it is a fixture supplied by
# syrupy, the snapshot testing library we're utilizing:
# https://docs.pytest.org/en/secure/fixture.html
def test_matches_snapshot(snapshot):
    # Arrange the app and sources within the different stack.
    app = cdk.App()
    topics_stack = cdk.Stack(app, "TopicsStack")
    matters = [sns.Topic(topics_stack, "Topic1")]

    # Create the ProcessorStack.
    processor_stack = ProcessorStack(
        app, "ProcessorStack", matters=matters  # Cross-stack reference
    )

    # Put together the stack for assertions.
    template = Template.from_stack(processor_stack)

    assert template.to_json() == snapshot

Testing Constructs

With the assertions module, constructs could be examined equally to stacks. Since there’s no stack to create, the distinction is that we create a stack that holds the assemble to be examined.

For instance, if we’ve got this DeadLetterQueue assemble (beforehand used within the Testing infrastructure with the AWS Cloud Growth Package weblog put up):

from aws_cdk import aws_cloudwatch as cloudwatch
from aws_cdk import aws_sqs as sqs
from aws_cdk import core as cdk

class DeadLetterQueue(sqs.Queue):
    def __init__(self, scope: cdk.Assemble, id: str):
        tremendous().__init__(scope, id)

        self.messages_in_queue_alarm = cloudwatch.Alarm(
            self,
            "Alarm",
            alarm_description="There are messages within the Useless Letter Queue.",
            evaluation_periods=1,
            threshold=1,
            metric=self.metric_approximate_number_of_messages_visible(),
        )

You possibly can take a look at it like this:

from aws_cdk import core as cdk
from aws_cdk.assertions import Match, Template

from app.dead_letter_queue import DeadLetterQueue

def test_creates_alarm():
    stack = cdk.Stack()
    DeadLetterQueue(stack, "DeadLetterQueue")

    template = Template.from_stack(stack)
    template.has_resource_properties(
        "AWS::CloudWatch::Alarm",
        {
            "Namespace": "AWS/SQS",
            "MetricName": "ApproximateNumberOfMessagesVisible",
            "Dimensions": [
                {
                    "Name": "QueueName",
                    "Value": Match.any_value(),
                },
            ],
        },
    )

Abstract

Testing is necessary to make sure that your code does what you anticipate and stop regressions and sudden adjustments when making adjustments. The brand new AWS CDK assertions module gives new and extra highly effective methods to check your infrastructure as code, particularly if you’re growing in a language the place the assert module isn’t out there. For extra info on the assertions module, seek advice from the API reference. As at all times, we welcome bug experiences, function requests, and pull requests on the aws-cdk GitHub repository.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments