Unit Testing Assets
Posted on
When deploying infrastructure, we want to ensure that what we’re deploying matches our expectations. One way to do so is via unit testing. We’ve talked about this concept in previous posts, such as in this overview and this post on deployments with .NET.
Often, when we’re creating cloud resources, we want to ensure that a resource’s underlying assets match certain properties. For example, the entrypoint or handler for a cloud function should be an executable function. Similarly, objects we’ll serve as static assets on a website should not exceed a certain size. We can use Pulumi’s unit testing framework along with language-specific constructs such as introspection or filesystem calls to ensure this type of correctness. We’ll walk through some examples to show just how easy it is to do so.
The examples below are in Python, but you can easily achieve the same types of tests using introspection/reflection in Node, .NET, and Go.
Check that Functions are Executable
When creating a Lambda function, we’re expected to pass in a function handler that gets called upon invocation.
Typically, the value is something like mod.handler
, where mod
is the module name and handler
is a function.
A basic check would be to make sure that the module exists and that the function is callable.
We can easily construct this unit test like so:
test_infra.py:
import unittest
import pulumi
class MyMocks(pulumi.runtime.Mocks):
def new_resource(self, type_, name, inputs, provider, id_):
return [name + '_id', inputs]
def call(self, token, args, provider):
return {}
pulumi.runtime.set_mocks(MyMocks())
import infra
class TestInfrastructure(unittest.TestCase):
# Test that the function entrypoint is actually an executable function.
@pulumi.runtime.test
def test_function_handler_callable(self):
def check_entrypoint(handler):
import importlib
from types import ModuleType
from inspect import signature, Parameter
# The handler should be of the form <module_name>.<function>
module_name, function_handler = handler.split('.', 1)
# Check that we can load the module.
module = importlib.import_module(module_name)
self.assertIsInstance(module, ModuleType)
# Check that the function handler is actually callable.
fn = getattr(module, function_handler)
self.assertTrue(callable(fn))
# Check that it has two parameters: event and context
sig = signature(fn)
self.assertListEqual(['event', 'context'], list(sig.parameters.keys()))
return infra.lambda_function.handler.apply(check_entrypoint)
Here, we use Python’s introspection capabilities to import a module based on a string that we obtain from the Lambda function definition. Similarly, after we import the module, we can verify that the function is callable and has the appropriate arguments for a Lambda function. At this point, we’ve confirmed that we’ve correctly “wired in” a function handler. Separately, we would expect another set of unit tests that verifies the correctness of the handler itself.
Here’s the corresponding infrastructure code to make the test pass,
assuming there’s also a file called lambda_function.py
, which contains a function handler(event, context)
.
infra.py:
import pulumi
import pulumi_aws as aws
lambda_role = aws.iam.Role('example-role', assume_role_policy=f"""{{
"Version":"2012-10-17",
"Statement":[{{
"Effect": "Allow",
"Principal":{{
"Service": "lambda.amazonaws.com"
}},
"Action": "sts:AssumeRole"
}}]
}}
""")
attach_lambda_role_execution = aws.iam.RolePolicyAttachment('example-attach-execute',
policy_arn='arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
role=lambda_role.name)
module_name = 'lambda_function'
module_file = f'{module_name}.py'
lambda_function = aws.lambda_.Function('example-function',
handler=f'{module_name}.handler',
role=lambda_role.arn,
runtime='python3.7',
code=pulumi.AssetArchive({
module_file: pulumi.FileAsset(module_file)
})
)
If lambda_function.py
doesn’t exist or there’s no function handler
within that module, the test will fail.
File Assets
Another example of checking for underlying asset correctness is when providing files to a resource. For example, we might want to check that the content type we provide to an object matches that of the resource:
test_infra.py:
# Test that the bucket object matches the correct mimetype
@pulumi.runtime.test
def test_bucket_object_content_type(self):
def check_filetype(args):
source, mimetype = args
import filetype
kind = filetype.guess(source.path)
self.assertEqual(kind.mime, mimetype)
return pulumi.Output.all(infra.bucket_obj.source, infra.bucket_obj.content_type).apply(check_filetype)
Here, we use the filetype
module to guess at the type based on reading the file itself.
We then check that the mime type specified on the bucket object matches.
For example, if the file represented by image.png
isn’t a PNG, this test would fail:
infra.py:
bucket = aws.s3.Bucket('my-bucket')
bucket_obj = aws.s3.BucketObject('my-bucket-obj',
bucket=bucket.id,
content_type='image/png',
source=pulumi.FileAsset('image.png'))
Secrets
When working with secrets, we want to ensure that provided values are secret and that corresponding outputs are secret. This helps ensure that our state never contains secrets as plaintext values. We can write a simple test to check this:
test_infra.py:
# Test that the secret input is marked as secret
@pulumi.runtime.test
def test_secret_input_and_output(self):
self.assertTrue(infra.secret_password_v1.secret_string.is_secret().result())
If we try to write something like the following in our code, it’ll fail this test:
infra.py:
secret_password = aws.secretsmanager.Secret('example-secret')
secret_password_v1 = aws.secretsmanager.SecretVersion('example-secret-version',
secret_id=secret_password,
secret_string='example-string')
If we change secret_string='example-string'
to secret_string=pulumi.Output.secret('example-string')
our test will pass.
Conclusion
There are many other examples of resources where it’s useful to check the underlying properties of the input assets. For example, checking the validity of certificates, keys, files, and container images are all assets that benefit from making sure we’re wiring in resources correctly to our cloud infrastructure. We hope you take this opportunity to use Pulumi’s unit testing framework to check your infrastructure’s underlying assets for correctness. Please visit the unit testing guide to learn more.