software.amazon.awscdk.integtests.alpha.package-info Maven / Gradle / Ivy
Show all versions of cdk-integ-tests-alpha Show documentation
/**
* integ-tests
*
* ---
*
*
*
*
*
* The APIs of higher level constructs in this module are experimental and under active development.
* They are subject to non-backward compatible changes or removal in any future version. These are
* not subject to the Semantic Versioning model and breaking changes will be
* announced in the release notes. This means that while you may use them, you may need to update
* your source code when upgrading to a newer version of this package.
*
*
*
*
*
*
*
*
Overview
*
* This library is meant to be used in combination with the integ-runner CLI
* to enable users to write and execute integration tests for AWS CDK Constructs.
*
* An integration test should be defined as a CDK application, and
* there should be a 1:1 relationship between an integration test and a CDK application.
*
* So for example, in order to create an integration test called my-function
* we would need to create a file to contain our integration test application.
*
* test/integ.my-function.ts
*
*
* App app = new App();
* Stack stack = new Stack();
* Function.Builder.create(stack, "MyFunction")
* .runtime(Runtime.NODEJS_LATEST)
* .handler("index.handler")
* .code(Code.fromAsset(join(__dirname, "lambda-handler")))
* .build();
*
*
* This is a self contained CDK application which we could deploy by running
*
*
* cdk deploy --app 'node test/integ.my-function.js'
*
*
* In order to turn this into an integration test, all that is needed is to
* use the IntegTest
construct.
*
*
* App app;
* Stack stack;
*
* IntegTest.Builder.create(app, "Integ").testCases(List.of(stack)).build();
*
*
* You will notice that the stack
is registered to the IntegTest
as a test case.
* Each integration test can contain multiple test cases, which are just instances
* of a stack. See the Usage section for more details.
*
*
Usage
*
*
IntegTest
*
* Suppose you have a simple stack, that only encapsulates a Lambda function with a
* certain handler:
*
*
* public class StackUnderTestProps extends StackProps {
* private Architecture architecture;
* public Architecture getArchitecture() {
* return this.architecture;
* }
* public StackUnderTestProps architecture(Architecture architecture) {
* this.architecture = architecture;
* return this;
* }
* }
*
* public class StackUnderTest extends Stack {
* public StackUnderTest(Construct scope, String id, StackUnderTestProps props) {
* super(scope, id, props);
*
* Function.Builder.create(this, "Handler")
* .runtime(Runtime.NODEJS_LATEST)
* .handler("index.handler")
* .code(Code.fromAsset(join(__dirname, "lambda-handler")))
* .architecture(props.getArchitecture())
* .build();
* }
* }
*
*
* You may want to test this stack under different conditions. For example, we want
* this stack to be deployed correctly, regardless of the architecture we choose
* for the Lambda function. In particular, it should work for both ARM_64
and
* X86_64
. So you can create an IntegTestCase
that exercises both scenarios:
*
*
* public class StackUnderTestProps extends StackProps {
* private Architecture architecture;
* public Architecture getArchitecture() {
* return this.architecture;
* }
* public StackUnderTestProps architecture(Architecture architecture) {
* this.architecture = architecture;
* return this;
* }
* }
*
* public class StackUnderTest extends Stack {
* public StackUnderTest(Construct scope, String id, StackUnderTestProps props) {
* super(scope, id, props);
*
* Function.Builder.create(this, "Handler")
* .runtime(Runtime.NODEJS_LATEST)
* .handler("index.handler")
* .code(Code.fromAsset(join(__dirname, "lambda-handler")))
* .architecture(props.getArchitecture())
* .build();
* }
* }
*
* // Beginning of the test suite
* App app = new App();
*
* IntegTest.Builder.create(app, "DifferentArchitectures")
* .testCases(List.of(
* new StackUnderTest(app, "Stack1", new StackUnderTestProps()
* .architecture(Architecture.ARM_64)
* ),
* new StackUnderTest(app, "Stack2", new StackUnderTestProps()
* .architecture(Architecture.X86_64)
* )))
* .build();
*
*
* This is all the instruction you need for the integration test runner to know
* which stacks to synthesize, deploy and destroy. But you may also need to
* customize the behavior of the runner by changing its parameters. For example:
*
*
* App app = new App();
*
* Stack stackUnderTest = new Stack(app, "StackUnderTest");
*
* Stack stack = new Stack(app, "stack");
*
* IntegTest testCase = IntegTest.Builder.create(app, "CustomizedDeploymentWorkflow")
* .testCases(List.of(stackUnderTest))
* .diffAssets(true)
* .stackUpdateWorkflow(true)
* .cdkCommandOptions(CdkCommands.builder()
* .deploy(DeployCommand.builder()
* .args(DeployOptions.builder()
* .requireApproval(RequireApproval.NEVER)
* .json(true)
* .build())
* .build())
* .destroy(DestroyCommand.builder()
* .args(DestroyOptions.builder()
* .force(true)
* .build())
* .build())
* .build())
* .build();
*
*
*
IntegTestCaseStack
*
* In the majority of cases an integration test will contain a single IntegTestCase
.
* By default when you create an IntegTest
an IntegTestCase
is created for you
* and all of your test cases are registered to this IntegTestCase
. The IntegTestCase
* and IntegTestCaseStack
constructs are only needed when it is necessary to
* defined different options for individual test cases.
*
* For example, you might want to have one test case where diffAssets
is enabled.
*
*
* App app;
* Stack stackUnderTest;
*
* IntegTestCaseStack testCaseWithAssets = IntegTestCaseStack.Builder.create(app, "TestCaseAssets")
* .diffAssets(true)
* .build();
*
* IntegTest.Builder.create(app, "Integ").testCases(List.of(stackUnderTest, testCaseWithAssets)).build();
*
*
*
Assertions
*
* This library also provides a utility to make assertions against the infrastructure that the integration test deploys.
*
* There are two main scenarios in which assertions are created.
*
*
* - Part of an integration test using
integ-runner
*
*
* In this case you would create an integration test using the IntegTest
construct and then make assertions using the assert
property.
* You should not utilize the assertion constructs directly, but should instead use the methods
on IntegTest.assertions
.
*
*
* App app;
* Stack stack;
*
*
* IntegTest integ = IntegTest.Builder.create(app, "Integ").testCases(List.of(stack)).build();
* integ.assertions.awsApiCall("S3", "getObject");
*
*
* By default an assertions stack is automatically generated for you. You may however provide your own stack to use.
*
*
* App app;
* Stack stack;
* Stack assertionStack;
*
*
* IntegTest integ = IntegTest.Builder.create(app, "Integ").testCases(List.of(stack)).assertionStack(assertionStack).build();
* integ.assertions.awsApiCall("S3", "getObject");
*
*
*
* - Part of a normal CDK deployment
*
*
* In this case you may be using assertions as part of a normal CDK deployment in order to make an assertion on the infrastructure
* before the deployment is considered successful. In this case you can utilize the assertions constructs directly.
*
*
* Stack myAppStack;
*
*
* AwsApiCall.Builder.create(myAppStack, "GetObject")
* .service("S3")
* .api("getObject")
* .build();
*
*
*
DeployAssert
*
* Assertions are created by using the DeployAssert
construct. This construct creates it's own Stack
separate from
* any stacks that you create as part of your integration tests. This Stack
is treated differently from other stacks
* by the integ-runner
tool. For example, this stack will not be diffed by the integ-runner
.
*
* DeployAssert
also provides utilities to register your own assertions.
*
*
* CustomResource myCustomResource;
* Stack stack;
* App app;
*
*
* IntegTest integ = IntegTest.Builder.create(app, "Integ").testCases(List.of(stack)).build();
* integ.assertions.expect("CustomAssertion", ExpectedResult.objectLike(Map.of("foo", "bar")), ActualResult.fromCustomResource(myCustomResource, "data"));
*
*
* In the above example an assertion is created that will trigger a user defined CustomResource
* and assert that the data
attribute is equal to { foo: 'bar' }
.
*
*
API Calls
*
* A common method to retrieve the "actual" results to compare with what is expected is to make an
* API call to receive some data. This library does this by utilizing CloudFormation custom resources
* which means that CloudFormation will call out to a Lambda Function which will
* make the API call.
*
*
HttpApiCall
*
* Using the HttpApiCall
will use the
* node-fetch JavaScript library to
* make the HTTP call.
*
* This can be done by using the class directory (in the case of a normal deployment):
*
*
* Stack stack;
*
*
* HttpApiCall.Builder.create(stack, "MyAsssertion")
* .url("https://example-api.com/abc")
* .build();
*
*
* Or by using the httpApiCall
method on DeployAssert
(when writing integration tests):
*
*
* App app;
* Stack stack;
*
* IntegTest integ = IntegTest.Builder.create(app, "Integ")
* .testCases(List.of(stack))
* .build();
* integ.assertions.httpApiCall("https://example-api.com/abc");
*
*
*
AwsApiCall
*
* Using the AwsApiCall
construct will use the AWS JavaScript SDK to make the API call.
*
* This can be done by using the class directory (in the case of a normal deployment):
*
*
* Stack stack;
*
*
* AwsApiCall.Builder.create(stack, "MyAssertion")
* .service("SQS")
* .api("receiveMessage")
* .parameters(Map.of(
* "QueueUrl", "url"))
* .build();
*
*
* Or by using the awsApiCall
method on DeployAssert
(when writing integration tests):
*
*
* App app;
* Stack stack;
*
* IntegTest integ = IntegTest.Builder.create(app, "Integ")
* .testCases(List.of(stack))
* .build();
* integ.assertions.awsApiCall("SQS", "receiveMessage", Map.of(
* "QueueUrl", "url"));
*
*
* You must specify the service
and the api
when using The AwsApiCall
construct.
* The service
is the name of an AWS service, in one of the following forms:
*
*
* - An AWS SDK for JavaScript v3 package name (
@aws-sdk/client-api-gateway
)
* - An AWS SDK for JavaScript v3 client name (
api-gateway
)
* - An AWS SDK for JavaScript v2 constructor name (
APIGateway
)
* - A lowercase AWS SDK for JavaScript v2 constructor name (
apigateway
)
*
*
* The api
is the name of an AWS API call, in one of the following forms:
*
*
* - An API call name as found in the API Reference documentation (
GetObject
)
* - The API call name starting with a lowercase letter (
getObject
)
* - The AWS SDK for JavaScript v3 command class name (
GetObjectCommand
)
*
*
* By default, the AwsApiCall
construct will automatically add the correct IAM policies
* to allow the Lambda function to make the API call. It does this based on the service
* and api
that is provided. In the above example the service is SQS
and the api is
* receiveMessage
so it will create a policy with Action: 'sqs:ReceiveMessage
.
*
* There are some cases where the permissions do not exactly match the service/api call, for
* example the S3 listObjectsV2
api. In these cases it is possible to add the correct policy
* by accessing the provider
object.
*
*
* App app;
* Stack stack;
* IntegTest integ;
*
*
* IApiCall apiCall = integ.assertions.awsApiCall("S3", "listObjectsV2", Map.of(
* "Bucket", "mybucket"));
*
* apiCall.provider.addToRolePolicy(Map.of(
* "Effect", "Allow",
* "Action", List.of("s3:GetObject", "s3:ListBucket"),
* "Resource", List.of("*")));
*
*
* Note that addToRolePolicy() uses direct IAM JSON policy blobs, not a iam.PolicyStatement
* object like you will see in the rest of the CDK.
*
*
EqualsAssertion
*
* This library currently provides the ability to assert that two values are equal
* to one another by utilizing the EqualsAssertion
class. This utilizes a Lambda
* backed CustomResource
which in tern uses the Match utility from the
* @aws-cdk/assertions library.
*
*
* App app;
* Stack stack;
* Queue queue;
* IFunction fn;
*
*
* IntegTest integ = IntegTest.Builder.create(app, "Integ")
* .testCases(List.of(stack))
* .build();
*
* integ.assertions.invokeFunction(LambdaInvokeFunctionProps.builder()
* .functionName(fn.getFunctionName())
* .invocationType(InvocationType.EVENT)
* .payload(JSON.stringify(Map.of("status", "OK")))
* .build());
*
* IApiCall message = integ.assertions.awsApiCall("SQS", "receiveMessage", Map.of(
* "QueueUrl", queue.getQueueUrl(),
* "WaitTimeSeconds", 20));
*
* message.assertAtPath("Messages.0.Body", ExpectedResult.objectLike(Map.of(
* "requestContext", Map.of(
* "condition", "Success"),
* "requestPayload", Map.of(
* "status", "OK"),
* "responseContext", Map.of(
* "statusCode", 200),
* "responsePayload", "success")));
*
*
*
Match
*
* integ-tests
also provides a Match
utility similar to the @aws-cdk/assertions
module. Match
* can be used to construct the ExpectedResult
. While the utility is similar, only a subset of methods are currently available on the Match
utility of this module: arrayWith
, objectLike
, stringLikeRegexp
and serializedJson
.
*
*
* AwsApiCall message;
*
*
* message.expect(ExpectedResult.objectLike(Map.of(
* "Messages", Match.arrayWith(List.of(Map.of(
* "Payload", Match.serializedJson(Map.of("key", "value"))), Map.of(
* "Body", Map.of(
* "Values", Match.arrayWith(List.of(Map.of("Asdf", 3))),
* "Message", Match.stringLikeRegexp("message"))))))));
*
*
*
Examples
*
*
Invoke a Lambda Function
*
* In this example there is a Lambda Function that is invoked and
* we assert that the payload that is returned is equal to '200'.
*
*
* IFunction lambdaFunction;
* App app;
*
*
* Stack stack = new Stack(app, "cdk-integ-lambda-bundling");
*
* IntegTest integ = IntegTest.Builder.create(app, "IntegTest")
* .testCases(List.of(stack))
* .build();
*
* IApiCall invoke = integ.assertions.invokeFunction(LambdaInvokeFunctionProps.builder()
* .functionName(lambdaFunction.getFunctionName())
* .build());
* invoke.expect(ExpectedResult.objectLike(Map.of(
* "Payload", "200")));
*
*
* The above example will by default create a CloudWatch log group that's never
* expired. If you want to configure it with custom log retention days, you need
* to specify the logRetention
property.
*
*
* import software.amazon.awscdk.services.logs.*;
*
* IFunction lambdaFunction;
* App app;
*
*
* Stack stack = new Stack(app, "cdk-integ-lambda-bundling");
*
* IntegTest integ = IntegTest.Builder.create(app, "IntegTest")
* .testCases(List.of(stack))
* .build();
*
* IApiCall invoke = integ.assertions.invokeFunction(LambdaInvokeFunctionProps.builder()
* .functionName(lambdaFunction.getFunctionName())
* .logRetention(RetentionDays.ONE_WEEK)
* .build());
*
*
*
Make an AWS API Call
*
* In this example there is a StepFunctions state machine that is executed
* and then we assert that the result of the execution is successful.
*
*
* App app;
* Stack stack;
* IStateMachine sm;
*
*
* IntegTest testCase = IntegTest.Builder.create(app, "IntegTest")
* .testCases(List.of(stack))
* .build();
*
* // Start an execution
* IApiCall start = testCase.assertions.awsApiCall("StepFunctions", "startExecution", Map.of(
* "stateMachineArn", sm.getStateMachineArn()));
*
* // describe the results of the execution
* IApiCall describe = testCase.assertions.awsApiCall("StepFunctions", "describeExecution", Map.of(
* "executionArn", start.getAttString("executionArn")));
*
* // assert the results
* describe.expect(ExpectedResult.objectLike(Map.of(
* "status", "SUCCEEDED")));
*
*
*
Chain ApiCalls
*
* Sometimes it may be necessary to chain API Calls. Since each API call is its own resource, all you
* need to do is add a dependency between the calls. There is an helper method next
that can be used.
*
*
* IntegTest integ;
*
*
* integ.assertions.awsApiCall("S3", "putObject", Map.of(
* "Bucket", "my-bucket",
* "Key", "my-key",
* "Body", "helloWorld")).next(integ.assertions.awsApiCall("S3", "getObject", Map.of(
* "Bucket", "my-bucket",
* "Key", "my-key")));
*
*
*
Wait for results
*
* A common use case when performing assertions is to wait for a condition to pass. Sometimes the thing
* that you are asserting against is not done provisioning by the time the assertion runs. In these
* cases it is possible to run the assertion asynchronously by calling the waitForAssertions()
method.
*
* Taking the example above of executing a StepFunctions state machine, depending on the complexity of
* the state machine, it might take a while for it to complete.
*
*
* App app;
* Stack stack;
* IStateMachine sm;
*
*
* IntegTest testCase = IntegTest.Builder.create(app, "IntegTest")
* .testCases(List.of(stack))
* .build();
*
* // Start an execution
* IApiCall start = testCase.assertions.awsApiCall("StepFunctions", "startExecution", Map.of(
* "stateMachineArn", sm.getStateMachineArn()));
*
* // describe the results of the execution
* IApiCall describe = testCase.assertions.awsApiCall("StepFunctions", "describeExecution", Map.of(
* "executionArn", start.getAttString("executionArn"))).expect(ExpectedResult.objectLike(Map.of(
* "status", "SUCCEEDED"))).waitForAssertions();
*
*
* When you call waitForAssertions()
the assertion provider will continuously make the awsApiCall
until the
* ExpectedResult
is met. You can also control the parameters for waiting, for example:
*
*
* IntegTest testCase;
* IApiCall start;
*
*
* IApiCall describe = testCase.assertions.awsApiCall("StepFunctions", "describeExecution", Map.of(
* "executionArn", start.getAttString("executionArn"))).expect(ExpectedResult.objectLike(Map.of(
* "status", "SUCCEEDED"))).waitForAssertions(WaiterStateMachineOptions.builder()
* .totalTimeout(Duration.minutes(5))
* .interval(Duration.seconds(15))
* .backoffRate(3)
* .build());
*
*/
@software.amazon.jsii.Stability(software.amazon.jsii.Stability.Level.Experimental)
package software.amazon.awscdk.integtests.alpha;