+++
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](/30-python/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.

If you have create the project with `cdk init` then you should already have a `tests` directory. In that case you will need to remove
the existing `test_cdk_workshop_stack.py` file.

If you do not already have a `tests` directory (usually created automatically when you run `cdk init`), then create a `tests` directory at the
root of the project and then create the following files:

```
mkdir -p tests/unit
touch tests/__init__.py
touch tests/unit/__init__.py
touch tests/unit/test_cdk_workshop.py
```

In the file called `test_cdk_workshop.py` create your first test using the following code.

```python
from aws_cdk import (
        Stack,
        aws_lambda as _lambda,
        assertions
    )
from cdk_workshop.hitcounter import HitCounter
import pytest


def test_dynamodb_table_created():
    stack = Stack()
    HitCounter(stack, "HitCounter",
            downstream=_lambda.Function(stack, "TestFunction",
                runtime=_lambda.Runtime.PYTHON_3_7,
                handler='hello.handler',
                code=_lambda.Code.from_asset('lambda')),
    )
    template = assertions.Template.from_stack(stack)
    template.resource_count_is("AWS::DynamoDB::Table", 1)
```

This test is simply testing to ensure that the synthesized stack includes a DynamoDB table.

Run the test.

```bash
$ pytest
```

You should see output like this:

```bash
$ pytest
================================================================================================= test session starts =================================================================================================
platform linux -- Python 3.9.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: ...
collected 1 item

tests/unit/test_cdk_workshop.py .                                                                                                                                                                               [100%]

================================================================================================== 1 passed in 1.49s ==================================================================================================
```

#### 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.

{{<highlight python "hl_lines=7-8">}}
self._handler = _lambda.Function(
    self, 'HitCounterHandler',
    runtime=_lambda.Runtime.PYTHON_3_7,
    handler='hitcount.handler',
    code=_lambda.Code.from_asset('lambda'),
    environment={
        'DOWNSTREAM_FUNCTION_NAME': downstream.function_name,
        'HITS_TABLE_NAME': self._table.table_name
    }
)
{{</highlight>}}

At this point we don't really know what the value of the `function_name` or `table_name` 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 `test_cdk_workshop.py` with the below code:

```python
def test_lambda_has_env_vars():
    stack = Stack()
    HitCounter(stack, "HitCounter",
            downstream=_lambda.Function(stack, "TestFunction",
                runtime=_lambda.Runtime.PYTHON_3_7,
                handler='hello.handler',
                code=_lambda.Code.from_asset('lambda')))

    template = assertions.Template.from_stack(stack)
    envCapture = assertions.Capture()

    template.has_resource_properties("AWS::Lambda::Function", {
        "Handler": "hitcount.handler",
        "Environment": envCapture,
        })

    assert envCapture.as_object() == {
            "Variables": {
                "DOWNSTREAM_FUNCTION_NAME": {"Ref": "TestFunctionXXXXX"},
                "HITS_TABLE_NAME": {"Ref": "HitCounterHitsXXXXXX"},
                },
            }
```

Save the file and run the test again.

```bash
pytest
```

This time the test should fail and you should be able to grab the correct value for the
variables from the expected output.

{{<highlight bash "hl_lines=17">}}
$ pytest
================================================================================================= test session starts =================================================================================================
platform linux -- Python 3.9.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: ...
collected 2 items

tests/unit/test_cdk_workshop.py .F                                                                                                                                                                              [100%]

====================================================================================================== FAILURES =======================================================================================================
______________________________________________________________________________________________ test_lambda_has_env_vars _______________________________________________________________________________________________

    ...


E       AssertionError: assert {'Variables':...ts079767E5'}}} == {'Variables':...tsXXXXXXXX'}}}
E         Differing items:
E         {'Variables': {'DOWNSTREAM_FUNCTION_NAME': {'Ref': 'TestFunction22AD90FC'}, 'HITS_TABLE_NAME': {'Ref': 'HitCounterHits079767E5'}}} != {'Variables': {'DOWNSTREAM_FUNCTION_NAME': {'Ref': 'TestFunctionXXXXXXXX'}, 'HITS_TABLE_NAME': {'Ref': 'HitCounterHitsXXXXXXXX'}}}
E         Use -v to get the full diff

tests/unit/test_cdk_workshop.py:35: AssertionError
=============================================================================================== short test summary info ===============================================================================================
FAILED tests/unit/test_cdk_workshop.py::test_lambda_has_env_vars - AssertionError: assert {'Variables':...ts079767E5'}}} == {'Variables':...tsXXXXXXXX'}}}
============================================================================================= 1 failed, 1 passed in 1.60s =============================================================================================
{{</highlight>}}

Grab the real values for the environment variables and update your test

{{<highlight python "hl_lines=16-17">}}
def test_lambda_has_env_vars():
    stack = Stack()
    HitCounter(stack, "HitCounter",
            downstream=_lambda.Function(stack, "TestFunction",
                runtime=_lambda.Runtime.PYTHON_3_7,
                handler='hello.handler',
                code=_lambda.Code.from_asset('lambda')))
    template = assertions.Template.from_stack(stack)
    envCapture = assertions.Capture()
    template.has_resource_properties("AWS::Lambda::Function", {
        "Handler": "hitcount.handler",
        "Environment": envCapture,
        })
    assert envCapture.as_object() == {
            "Variables": {
                "DOWNSTREAM_FUNCTION_NAME": {"Ref": "REPLACE_VALUE_HERE"},
                "HITS_TABLE_NAME": {"Ref": "REPLACE_VALUE_HERE"},
                },
            }

{{</highlight>}}

Now run the test again. This time is should pass.

```bash
$ pytest
```

You should see output like this:

```bash
$ pytest

================================================================================================= test session starts =================================================================================================
platform linux -- Python 3.9.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: ...
collected 2 items

tests/unit/test_cdk_workshop.py ..                                                                                                                                                                              [100%]

================================================================================================== 2 passed in 1.58s ==================================================================================================
```

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.

{{<highlight python>}}
def test_dynamodb_with_encryption():
    stack = Stack()
    HitCounter(stack, "HitCounter",
            downstream=_lambda.Function(stack, "TestFunction",
                runtime=_lambda.Runtime.PYTHON_3_7,
                handler='hello.handler',
                code=_lambda.Code.from_asset('lambda')))

    template = assertions.Template.from_stack(stack)
    template.has_resource_properties("AWS::DynamoDB::Table", {
        "SSESpecification": {
            "SSEEnabled": True,
            },
        })
{{</highlight>}}

Now run the test, which should fail.

```bash
$ pytest
================================================================================================= test session starts =================================================================================================
platform linux -- Python 3.9.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: ...
collected 3 items

tests/unit/test_cdk_workshop.py ..F                                                                                                                                                                             [100%]

====================================================================================================== FAILURES =======================================================================================================
____________________________________________________________________________________________ test_dynamodb_with_encryption ____________________________________________________________________________________________
jsii.errors.JavaScriptError:
  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)
      at Template.hasResourceProperties (/tmp/jsii-kernel-4Be5Dy/node_modules/@aws-cdk/assertions/lib/template.js:85:19)
      at /tmp/tmpdj0kczj7/lib/program.js:8248:134
      at Kernel._wrapSandboxCode (/tmp/tmpdj0kczj7/lib/program.js:8860:24)
      at /tmp/tmpdj0kczj7/lib/program.js:8248:107
      at Kernel._ensureSync (/tmp/tmpdj0kczj7/lib/program.js:8841:28)
      at Kernel.invoke (/tmp/tmpdj0kczj7/lib/program.js:8248:34)
      at KernelHost.processRequest (/tmp/tmpdj0kczj7/lib/program.js:9757:36)
      at KernelHost.run (/tmp/tmpdj0kczj7/lib/program.js:9720:22)
      at Immediate._onImmediate (/tmp/tmpdj0kczj7/lib/program.js:9721:46)
      at processImmediate (internal/timers.js:464:21)

The above exception was the direct cause of the following exception:

    ...more error info...

=============================================================================================== short test summary info ===============================================================================================
FAILED tests/unit/test_cdk_workshop.py::test_dynamodb_with_encryption - jsii.errors.JSIIError: Template has 1 resources with type AWS::DynamoDB::Table, but none match as expected.
============================================================================================= 1 failed, 2 passed in 1.65s =============================================================================================
```

Now lets fix the broken test. Update the hitcounter code to enable encryption by default.

Edit `hitcounter.py`
{{<highlight python "hl_lines=4">}}
self._table = ddb.Table(
    self, 'Hits',
    partition_key={'name': 'path', 'type': ddb.AttributeType.STRING},
    encryption=ddb.TableEncryption.AWS_MANAGED,
)
{{</highlight>}}

Now run the test again, which should now pass.

```bash
$ pytest

================================================================================================= test session starts =================================================================================================
platform linux -- Python 3.9.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: ...
collected 3 items

tests/unit/test_cdk_workshop.py ...                                                                                                                                                                             [100%]

================================================================================================== 3 passed in 1.59s ==================================================================================================
```