AWS CDK with Automate Testing and TDD
Tags: aws, awscdk, testing, tdd
Published: 2023-02-14 05:48:33

This post is third post after [this(Part2)](https://dev.to/kkkensuke/aws-cdk-for-lambda-and-dynamodb-1bn9).

## Environment

- AWS Cloud9
- CDK Version : 2.63.2

## What we do
This time, we will perform the Fine-Grained Assertions test (Assertion Test) as described in the official [AWS documentation](https://docs.aws.amazon.com/cdk/v2/guide/testing.html#testing_fine_grained).
In this test script, we will mainly use the hasResourceProperties function. This helper function is used when creating a resource in CDK to check "whether the resource has been created" and "the property values set for the resource.

## Assertion Tests
### Create a test that the DynamoDB table has been created
First, delete the unnecessary test/cdk-workshop.test.ts file. Then create the file test/hitcounter.test.ts and write the following code.
```
import { Template, Capture } from 'aws-cdk-lib/assertions';
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { HitCounter } from '../lib/hitcounter';

test('DynamoDB Table Created', () => {
  const stack = new cdk.Stack();
  
  // WHEN
  new HitCounter(stack, 'MyTestConstruct', {
    downstream: new lambda.Function(stack, 'TestFunction', {
      runtime: lambda.Runtime.NODEJS_14_X,
      handler: 'hello.handler',
      code: lambda.Code.fromAsset('lambda')
    })
  });
  
  // THEN
  const template = Template.fromStack(stack);
  template.resourceCountIs("AWS::DynamoDB::Table", 1);
});
```
This test simply tests that the stack contains a DynamoDB table.
Run the test.
```
$ npm run test
```
The following results will be displayed
```
$ npm run test

> cdk-workshop@0.1.0 test
> jest

 PASS  test/hitcounter.test.ts (12.242 s)
  ✓ DynamoDB Table Created (177 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        12.439 s
Ran all test suites.
```
### Create a test that Lambda Function has been created
Next, we will add a test case for the Lambda function. This time, in addition to test the creation of the Lambda function, we will also test the values of two environment variables (DOWNSTREAM_FUNCTION_NAME and HITS_TABLE_NAME). You specified these two environment variables when creating the Lambda function in Part 2.
At this point, we do not know which values will be set for these two environment variables (these values were determined at the time of deployment), so we set dummy values for them. Therefore, the first test will fail, but the actual values of the environment variables are output in the failure log, so we will use those values later.
Add a new test case as follows.
```
test('Lambda Has Environment Variables', () => {
  const stack = new cdk.Stack();
  
  // WHEN
  new HitCounter(stack, 'MyTestConstruct', {
    downstream: new lambda.Function(stack, 'TestFunctioin', {
      runtime: lambda.Runtime.NODEJS_14_X,
      handler: 'hello.handler',
      code: lambda.Code.fromAsset('lambda')
    })
  });
  
  // THEN
  const template = Template.fromStack(stack);
  const envCapture = new Capture();
  template.hasResourceProperties("AWS::Lambda::Function", {
    Environment: envCapture,
  });
  
  expect(envCapture.asObject()).toEqual(
    {
      Variables: {
        DOWNSTREAM_FUNCTION_NAME: {
          Ref: "TestFunctionXXXX",
        },
        HITS_TABLE_NAME: {
          Ref: "MyTestConstructHitsXXXX",
        }
      },
    }
  );
});
```
After saving the file, run the test.
```
$ npm run test
```
One error is included in the test results as follows. The error contains the value of an environment variable that should be specified, so we will use this one.
```
$ npm run test

> cdk-workshop@0.1.0 test
> jest

 FAIL  test/hitcounter.test.ts (13.577 s)
  ✓ DynamoDB Table Created (198 ms)
  ✕ Lambda Has Environment Variables (99 ms)

  ● Lambda Has Environment Variables

    expect(received).toEqual(expected) // deep equality

    - Expected  - 2
    + Received  + 2

      Object {
        "Variables": Object {
          "DOWNSTREAM_FUNCTION_NAME": Object {
    -       "Ref": "TestFunctionXXXX",
    +       "Ref": "TestFunctioin03257049",
          },
          "HITS_TABLE_NAME": Object {
    -       "Ref": "MyTestConstructHitsXXXX",
    +       "Ref": "MyTestConstructHits24A357F0",
          },
        },
      }

      41 |   });
      42 |   
    > 43 |   expect(envCapture.asObject()).toEqual(
         |                                 ^
      44 |     {
      45 |       Variables: {
      46 |         DOWNSTREAM_FUNCTION_NAME: {

      at Object.<anonymous> (test/hitcounter.test.ts:43:33)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        13.766 s
Ran all test suites.
```
Now rewrite the contents of the test case using the values displayed in the above error.
```
...
expect(envCapture.asObject()).toEqual(
    {
      Variables: {
        DOWNSTREAM_FUNCTION_NAME: {
          Ref: "UPDATE Value from error log",
        },
        HITS_TABLE_NAME: {
          Ref: "UPDATE Value from error log",
        }
      },
    }
  );
...
```
I ran the test again, and this time it worked!
```
 $ npm run test

> cdk-workshop@0.1.0 test
> jest

 PASS  test/hitcounter.test.ts (12.738 s)
  ✓ DynamoDB Table Created (150 ms)
  ✓ Lambda Has Environment Variables (94 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        12.942 s, estimated 14 s
Ran all test suites.
```
### TDD (Test Driven Development）
It is also possible to use the TDD methodology when creating a stack in CDK. As a simple example, suppose we have a new requirement to encrypt an existing DynamoDB table.
The TDD approach would first reflect this requirement in a test script.
```
test('DynamoDB Table Created With Encryption', () => {
  const stack = new cdk.Stack();
  
  // WHEN
  new HitCounter(stack, 'MyTestConstruct', {
    downstream: new lambda.Function(stack, 'TestFunction', {
      runtime: lambda.Runtime.NODEJS_14_X,
      handler: 'hello.handler',
      code: lambda.Code.fromAsset('lambda')
    })
  });
  
  // THEN
  const template = Template.fromStack(stack);
  template.hasResourceProperties("AWS::DynamoDB::Table", {
    SSESpecification: {
      SSEEnabled: true
    }
  });
});
```
When I run the test, I think it will be an error. This is to be expected since we did not include the encryption setting in the constructor.
So let's update the constructor to state that it will be encrypted for the DynamoDB table.
```
...
 constructor(scope: Construct, id: string, props: HitCounterProps) {
    super(scope, id);
    
    const table = new dynamodb.Table(this, 'Hits', {
      partitionKey: { name: 'path', type: dynamodb.AttributeType.STRING },
      encryption: dynamodb.TableEncryption.AWS_MANAGED
    });
...
```
Run the test again and see if it succeeds this time.
So this is how CDK could be used to approach TDD!

## Summary
What do you think? I think there are many projects where visual checks are done on the console, but with the CDK, we can test whether the AWS services are being built as expected, and the DevOps cycle will be faster. 

## Reference
https://cdkworkshop.com/20-typescript/70-advanced-topics/100-construct-testing.html