Fine-Grained Assertion Tests #
Create a test for the DynamoDB table #
This section assumes that you have created the hit counter construct
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.
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.
$ pytest
You should see output like this:
$ 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.
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
}
)
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:
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.
pytest
This time the test should fail and you should be able to grab the correct value for the variables from the expected output.
$ 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 =============================================================================================
Grab the real values for the environment variables and update your test
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"},
},
}
Now run the test again. This time is should pass.
$ pytest
You should see output like this:
$ 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.
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,
},
})
Now run the test, which should fail.
$ 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
self._table = ddb.Table(
self, 'Hits',
partition_key={'name': 'path', 'type': ddb.AttributeType.STRING},
encryption=ddb.TableEncryption.AWS_MANAGED,
)
Now run the test again, which should now pass.
$ 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 ==================================================================================================