+++ title = "Assertion Tests" weight = 200 +++ ### Fine-Grained Assertion Tests #### Create a test for the DynamoDB table {{% notice info %}} This section assumes that you have [created the hit counter construct](/50-java/40-hit-counter.html) {{% /notice %}} Our `HitCounter` construct creates a simple DynamoDB table. Lets create a test that validates that the table is getting created. Since we removed the `src/test` directory (usually created automatically when you run `cdk init`), we need to create a new `test` directory under `src`: ```bash mkdir -p src/test/java/com/myorg ``` And then create a file called `HitCounterTest.java` with the following code. ```java package com.myorg; import software.amazon.awscdk.Stack; import software.amazon.awscdk.assertions.Template; import software.amazon.awscdk.assertions.Capture; import java.io.IOException; import software.amazon.awscdk.services.lambda.Code; import software.amazon.awscdk.services.lambda.Function; import software.amazon.awscdk.services.lambda.Runtime; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; public class HitCounterTest { @Test public void testDynamoDBTable() throws IOException { Stack stack = new Stack(); Function hello = Function.Builder.create(stack, "HelloHandler") .runtime(Runtime.NODEJS_14_X) .code(Code.fromAsset("lambda")) .handler("hello.handler") .build(); HitCounter helloWithCounter = new HitCounter(stack, "HelloHitCounter", HitCounterProps.builder() .downstream(hello) .build()); // synthesize the stack to a CloudFormation template Template template = Template.fromStack(stack); template.resourceCountIs("AWS::DynamoDB::Table", 1); } } ``` This test is simply testing to ensure that the synthesized stack includes a DynamoDB table. Run the test. ```bash $ mvn test ``` You should see output like this: ```bash $ mvn test ...building info... ------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.myorg.HitCounterTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.644 sec Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 9.316 s [INFO] Finished at: 2021-10-29T20:02:43Z [INFO] ------------------------------------------------------------------------ ``` #### Create a test for the Lambda function Now lets add another test, this time for the Lambda function that the `HitCounter` construct creates. This time in addition to testing that the Lambda function is created, we also want to test that it is created with the two environment variables `DOWNSTREAM_FUNCTION_NAME` & `HITS_TABLE_NAME`. Add another test below the DynamoDB test. If you remember, when we created the lambda function the environment variable values were references to other constructs. {{}} final Map environment = new HashMap<>(); environment.put("DOWNSTREAM_FUNCTION_NAME", props.getDownstream().getFunctionName()); environment.put("HITS_TABLE_NAME", this.table.getTableName()); this.handler = Function.Builder.create(this, "HitCounterHandler") .runtime(Runtime.NODEJS_14_X) .handler("hitcounter.handler") .code(Code.fromAsset("lambda")) .environment(environment) .build(); {{}} At this point we don't really know what the value of the `functionName` or `tableName` will be since the CDK will calculate a hash to append to the end of the name of the constructs, so we will just use a dummy value for now. Once we run the test it will fail and show us the expected value. Create a new test in `HitCounterTest.Java` with the below code: ```java @Test public void testLambdaEnvVars() throws IOException { Stack stack = new Stack(); Function hello = Function.Builder.create(stack, "HelloHandler") .runtime(Runtime.NODEJS_14_X) .code(Code.fromAsset("lambda")) .handler("hello.handler") .build(); HitCounter helloWithCounter = new HitCounter(stack, "HelloHitCounter", HitCounterProps.builder() .downstream(hello) .build()); // synthesize the stack to a CloudFormation template Template template = Template.fromStack(stack); Capture envCapture = new Capture(); Map expected = Map.of( "Handler", "hitcounter.handler", "Environment", envCapture); template.hasResourceProperties("AWS::Lambda::Function", expected); Map expectedEnv = Map.of( "Variables", Map.of( "DOWNSTREAM_FUNCTION_NAME", Map.of("Ref", "HelloHandlerXXXXXXXXX"), "HITS_TABLE_NAME", Map.of("Ref", "HelloHitCounterHitsXXXXXXXXX") ) ); assertThat(envCapture.asObject()).isEqualTo(expectedEnv); } ``` Save the file and run the test again. ```bash $ mvn test ``` This time the test should fail and you should be able to grab the correct value for the variables from the expected output. {{}} $ mvn test ------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.myorg.HitCounterTest Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.792 sec <<< FAILURE! com.myorg.HitCounterTest.testLambdaEnvVars() Time elapsed: 0.106 sec <<< FAILURE! org.opentest4j.AssertionFailedError: Expecting: <{"Variables"={"DOWNSTREAM_FUNCTION_NAME"={"Ref"="HelloHandler2E4FBA4D"}, "HITS_TABLE_NAME"={"Ref"="HelloHitCounterHits7AAEBF80"}}}> to be equal to: <{"Variables"={"DOWNSTREAM_FUNCTION_NAME"={"Ref"="HelloHandlerXXXXXXXXX"}, "HITS_TABLE_NAME"={"Ref"="HelloHitCounterHitsXXXXXXXXX"}}}> but was not. at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:78) at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) at com.myorg.HitCounterTest.testLambdaEnvVars(HitCounterTest.java:70) Results : Failed tests: com.myorg.HitCounterTest.testLambdaEnvVars(): (..) Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 13.296 s [INFO] Finished at: 2021-11-01T12:37:23Z [INFO] ------------------------------------------------------------------------ {{}} Grab the real values for the environment variables and update your test {{}} @Test public void testLambdaEnvVars() throws IOException { Stack stack = new Stack(); Function hello = Function.Builder.create(stack, "HelloHandler") .runtime(Runtime.NODEJS_14_X) .code(Code.fromAsset("lambda")) .handler("hello.handler") .build(); HitCounter helloWithCounter = new HitCounter(stack, "HelloHitCounter", HitCounterProps.builder() .downstream(hello) .build()); // synthesize the stack to a CloudFormation template Template template = Template.fromStack(stack); Capture envCapture = new Capture(); Map expected = Map.of( "Handler", "hitcounter.handler", "Environment", envCapture); template.hasResourceProperties("AWS::Lambda::Function", expected); Map expectedEnv = Map.of( "Variables", Map.of( "DOWNSTREAM_FUNCTION_NAME", Map.of("Ref", "REPLACE_VALUE_HERE"), "HITS_TABLE_NAME", Map.of("Ref", "REPLACE_VALUE_HERE") ) ); assertThat(envCapture.asObject()).isEqualTo(expectedEnv); } {{}} Now run the test again. This time is should pass. ```bash $ mvn test ``` You should see output like this: ```bash $ mvn test ------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.myorg.HitCounterTest Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.785 sec Results : Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 9.571 s [INFO] Finished at: 2021-11-01T12:42:03Z [INFO] ------------------------------------------------------------------------ ``` You can also apply TDD (Test Driven Development) to developing CDK Constructs. For a very simple example, lets add a new requirement that our DynamoDB table be encrypted. First we'll update the test to reflect this new requirement. {{}} @Test public void testDynamoDBEncryption() throws IOException { Stack stack = new Stack(); Function hello = Function.Builder.create(stack, "HelloHandler") .runtime(Runtime.NODEJS_14_X) .code(Code.fromAsset("lambda")) .handler("hello.handler") .build(); HitCounter helloWithCounter = new HitCounter(stack, "HelloHitCounter", HitCounterProps.builder() .downstream(hello) .build()); // synthesize the stack to a CloudFormation template Template template = Template.fromStack(stack); Capture envCapture = new Capture(); Map expected = Map.of( "SSESpecification", Map.of("SSEEnabled", true)); template.hasResourceProperties("AWS::DynamoDB::Table", expected); } {{}} Now run the test, which should fail. ```bash $ mvn test ------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.myorg.HitCounterTest Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.805 sec <<< FAILURE! com.myorg.HitCounterTest.testDynamoDBEncryption() Time elapsed: 0.043 sec <<< FAILURE! software.amazon.jsii.JsiiException: Template has 1 resources with type AWS::DynamoDB::Table, but none match as expected. The closest result is: { "Type": "AWS::DynamoDB::Table", "Properties": { "KeySchema": [ { "AttributeName": "path", "KeyType": "HASH" } ], "AttributeDefinitions": [ { "AttributeName": "path", "AttributeType": "S" } ], "ProvisionedThroughput": { "ReadCapacityUnits": 5, "WriteCapacityUnits": 5 } }, "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" } with the following mismatches: Missing key at /Properties/SSESpecification (using objectLike matcher) Error: Template has 1 resources with type AWS::DynamoDB::Table, but none match as expected. The closest result is: { "Type": "AWS::DynamoDB::Table", "Properties": { "KeySchema": [ { "AttributeName": "path", "KeyType": "HASH" } ], "AttributeDefinitions": [ { "AttributeName": "path", "AttributeType": "S" } ], "ProvisionedThroughput": { "ReadCapacityUnits": 5, "WriteCapacityUnits": 5 } }, "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" } with the following mismatches: Missing key at /Properties/SSESpecification (using objectLike matcher) Results : Failed tests: com.myorg.HitCounterTest.testDynamoDBEncryption(): Template has 1 resources with type AWS::DynamoDB::Table, but none match as expected.(..) Tests run: 3, Failures: 1, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 10.141 s [INFO] Finished at: 2021-11-01T12:44:41Z [INFO] ------------------------------------------------------------------------ ``` Now lets fix the broken test. Update the hitcounter code to enable encryption by default. {{}} package com.myorg; import java.util.HashMap; import java.util.Map; import software.constructs.Construct; import software.amazon.awscdk.services.dynamodb.Attribute; import software.amazon.awscdk.services.dynamodb.AttributeType; import software.amazon.awscdk.services.dynamodb.Table; import software.amazon.awscdk.services.dynamodb.TableEncryption; import software.amazon.awscdk.services.lambda.Code; import software.amazon.awscdk.services.lambda.Function; import software.amazon.awscdk.services.lambda.Runtime; public class HitCounter extends Construct { private final Function handler; private final Table table; public HitCounter(final Construct scope, final String id, final HitCounterProps props) { super(scope, id); this.table = Table.Builder.create(this, "Hits") .partitionKey(Attribute.builder() .name("path") .type(AttributeType.STRING) .build()) .encryption(TableEncryption.AWS_MANAGED) .build(); final Map environment = new HashMap<>(); environment.put("DOWNSTREAM_FUNCTION_NAME", props.getDownstream().getFunctionName()); environment.put("HITS_TABLE_NAME", this.table.getTableName()); this.handler = Function.Builder.create(this, "HitCounterHandler") .runtime(Runtime.NODEJS_14_X) .handler("hitcounter.handler") .code(Code.fromAsset("lambda")) .environment(environment) .build(); } /** * @return the counter definition */ public Function getHandler() { return this.handler; } /** * @return the counter table */ public Table getTable() { return this.table; } } {{}} Now run the test again, which should now pass. ```bash $ mvn test ------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.myorg.HitCounterTest Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.807 sec Results : Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 10.072 s [INFO] Finished at: 2021-11-01T12:46:58Z [INFO] ------------------------------------------------------------------------ ```