Snapshot Testing With Syrupy in AWS CDK Python
In 2023, I wrote a post on snapshot testing in AWS CDK Python. Since then, I’ve switched from pytest-snapshot
to syrupy
.
At Defiance Digital, we use the AWS CDK for almost everything. Generally we use TypeScript because it’s the original language for the CDK, everything using JSII transpiles back to TypeScript, and it has the most compability with the CDK. However, we have a few projects that use Python, and on those I’ve really been missing Jest snapshot testing.
For most CDK projects snapshot tests are the perfect way to make sure that your stacks don’t have unintended changes. We include fine-grained tests as the stacks are deployed and mature. However, every stack always starts with a snapshot test. So how do we do that in Python?
The code
First, let’s have a look at the dev
group of our pyproject.toml
file and the overall test suite for this stack. Note that we are using Poetry to manage our environment and dependencies via projen.
pyproject.toml
[tool.poetry.dev-dependencies]
bandit = "*"
ruff = "*"
projen = "*"
syrupy = "*" # This is the key dependency
pytest = "*"
The syrupy
dependency is what allows us to take snapshot tests. We also need to have pytest
available as the testing framework.
test_customer_stack.py
from json import dumps
import pytest
from aws_cdk import App
from aws_cdk.assertions import Template
from python_service.stacks.ecr_repo_stack import EcrRepoStack
@pytest.fixture(scope="module")
def template():
app = App()
stack = EcrRepoStack(app, "my-stack-test", repository_name="python-service")
template = Template.from_stack(stack)
yield template
def test_ecr_repo_found(template):
template.resource_count_is("AWS::ECR::Repository", 1)
def test_snapshot(template, snapshot):
assert dumps(template.to_json()) == snapshot
What’s happening here, exactly?
First, we import all our dependencies, which are just Python standard library, pytest
, aws-cdk
, and our CDK stack. Next, we define a Pytest fixture that can be used in our test definitions. This fixture is a Template
object from the aws-cdk
library, which is a representation of the CloudFormation template that will be generated by our stack. We can use this same fixture for both fine-grained tests and snapshot tests.
The test_ecr_repo_found
function is a fine-grained test. It uses the Template
object to assert that there is exactly one AWS::ECR::Repository
resource in the template. This is a simple test that ensures that our stack is generating the correct resources. It’s here as an example of a very simple fine-grained test. Generally as the stack mature we will add more fine-grained tests to ensure that the stack is generating the correct resources and that they are configured correctly.
Finally, the test_snapshot
function takes our template
fixture and a snapshot
argument from syrupy
. It uses the snapshot
fixture to assert that the template matches the snapshot. If the snapshot does not exist, it will be created. If it does exist, it will be compared to the current template. If the template has changed, the test will fail. If the template has not changed, the test will pass.
Running the tests
As I mentioned earlier, we’re using projen to manage our project. Projen has a built-in test
command that will run all the tests in the project. At Defiance, we use that to run our snapshot tests, both manually and in CI/CD pipelines.
Here’s what our .projenrc.py
snippet looks like to patch our test
task to use Poetry:
test_task = project.tasks.try_find("test")
if test_task:
test_task.reset("poetry run pytest tests/ --snapshot-update", receive_args=True)
Now we can run npx projen test
to run our tests.
If you aren’t using projen, you can simply run poetry run pytest tests/
to run the tests, assuming they are in a ./tests
folder. If you aren’t using Poetry, you can run pytest tests/
to run the tests.
Note that after the first snapshot creation, you may want to remove --snapshot-update
from your test
task so tests fail if something changes. If this is the case, consider adding a test:update
task to update snapshots during local development:
project.add_task(name="test:update", exec="poetry run pytest tests/ --snapshot-update", receive_args=True)
test_task = project.tasks.try_find("test")
if test_task:
test_task.reset("poetry run pytest tests/", receive_args=True)
Now we can run npx projen test:update
to update our test snapshots.
If you aren’t using projen, you can simply run poetry run pytest --snapshot-update tests/
to update the snapshots. If you aren’t using Poetry, you can run pytest --snapshot-update tests/
to update the snapshots.
Note that the snapshot test will fail the first time you run this command. After that, your tests should pass. If you’re running your tests in a CI/CD pipeline be sure to run the update command first locally, then the test command.
Conclusion
Snapshot testing is a great way to ensure that your CDK stacks don’t have unintended changes. It’s a great way to get started with testing your CDK stacks and ensure that your stacks don’t have unintended changes as they mature. I missed this feature in our Python projects but with this pattern, we can have the same functionality in Python as we do in TypeScript.
At Defiance Digital, our mission is to empower startups and SMBs to achieve their full potential by delivering reliable, secure, and scalable end-to-end managed services tailored to their unique needs. We do this by providing personalized attention and exceptional results through direct access to elite cloud engineers who embrace our “customers as co-workers” ethos.
Please feel free to reach out with questions, comments, or suggested improvements at either michael.gray@defiance.ai or mike@graywind.org.