AWS Developer Blog

Introducing the AWS Chalice test client

The latest release of AWS Chalice, v1.17.0, now includes a test client that enables you to write tests for your Chalice applications using a concise and simplified API. The test client handles all the boilerplate setup and teardown logic that you’d previously have to write yourself when testing your Chalice applications.

This new test client allows you to test REST APIs as well as all the event handlers for AWS Lambda supported by Chalice.

Basic Usage

To show how to use this new test client, let’s start with a hello world REST API. If you haven’t used Chalice before, you can follow our quickstart guide which will walk you through installing, configuring, and creating your first Chalice application.

First, we’ll update our app.py file with two routes.

from chalice import Chalice

app = Chalice(app_name='testclient')


@app.route('/')
def index():
    return {'hello': 'world'}


@app.route('/hello/{name}')
def hello(name):
    return {'hello': name}

To test this API, we’ll create a tests directory and create a tests/__init__.py and a tests/test_app.py file with two tests, one for each route.

$ mkdir tests
$ touch tests/{__init__.py,test_app.py}
$ tree
.
├── app.py
├── requirements.txt
└── tests
    ├── __init__.py
    └── test_app.py

Your tests/test_app.py should look like this.

import app
from chalice.test import Client


def test_index_route():
    with Client(app.app) as client:
        response = client.http.get('/')
        assert response.status_code == 200
        assert response.json_body == {'hello': 'world'}


def test_hello_route():
    with Client(app.app) as client:
        response = client.http.get('/hello/myname')
        assert response.status_code == 200
        assert response.json_body == {'hello': 'myname'}

In our test file above, we first import our test client from chalice.test. Next, in order to use our test client, we instantiate it and use it as a context manager. This ensures that our test environment is properly set up, and that on teardown we cleanup and replace any resources we needed to modify during our test, such as environment variables.

The test client has several attributes that you can use to help you write tests:

  • client.http – Used to test REST APIs.
  • client.lambda_ – Used to test Lambda functions by specifying the payload to pass to the Lambda function.
  • client.events – Used to generate sample events when testing Lambda functions through the client.lambda_ attribute.

To run our tests, we’ll install pytest.

$ pip install pytest
$ py.test tests/test_app.py
============================= test session starts ==============================
platform darwin -- Python 3.7.3, pytest-5.3.1, py-1.5.3, pluggy-0.12.0
rootdir: /tmp/testclient
plugins: hypothesis-4.43.1, cov-2.8.1
collected 2 items

test_app.py ..                                                                [100%]

============================= 2 passed in 0.32s ================================

Testing AWS Lambda functions

To test Lambda functions directly, we’ll use the client.lambda_.invoke() method. First, let’s test a Lambda function that isn’t connected to any events. Add this function to your app.py file.

@app.lambda_function()
def myfunction(event, context):
    return {'event': event}

Our test for this function will look similar to our REST API unit tests, except we’ll use the client.lambda_.invoke() method instead.

def test_my_function():
    with Client(app.app) as client:
        response = client.lambda_.invoke('myfunction',
                                         {'hello': 'world'})
        assert response.payload == {'event': {'hello': 'world'}}

Testing event handlers

In the previous example, we’re creating our own event payload to pass to our Lambda function invocation. We can use the client.events attribute to generate sample events for specific services that Chalice supports. To learn more about Lambda event sources with Chalice, see our event sources documentation.

Suppose we wanted to test an event handler connected to an Amazon SNS topic.

@app.on_sns_message(topic='mytopic')
def myfunction(event):
    return {'message': event.message}

In order to test this function we need to generate an event payload that matches the schema expected by this event handler. We can use the client.events.generate_sns_event() to do this for us:

def test_my_function():
    with Client(app.app) as client:
        event = client.events.generate_sns_event(message='hello world')
        response = client.lambda_.invoke('sns_message_handler', event)
        assert response.payload == {'message': 'hello world'}

Testing with the AWS SDK for Python

Finally, let’s look at an example that involves the AWS SDK for Python. In this example, we have a REST API that takes the request body and forwards it to Amazon S3 using the AWS SDK for Python, boto3.

_S3 = None


def get_s3_client():
    global _S3
    if _S3 is None:
        _S3 = boto3.client('s3')
    return _S3


@app.route('/resources/{name}', methods=['PUT'])
def send_to_s3(name):
    s3 = get_s3_client()
    s3.put_object(
        Bucket='mybucket',
        Key=name,
        Body=app.current_request.raw_body
    )
    return Response(status_code=204, body='')

In our test we want to verify that when we make a PUT request to /resources/myobject the request body is sent as the object body for a corresponding PutObject S3 API call. To test this, we’ll use the Chalice test client as well as Botocore Stubber. The Botocore Stubber allows us to stub out requests to AWS so you don’t actually send requests to AWS. It will validate that your stubbed input parameters and response values match the schema of the service API. To use the stubber, we wrap our S3 client in a Stubber instance and specify the expected API calls. We can use the stubber as a context manager which will automatically activate our stubber as well as cleanup once we’re finished using it. Here’s how this test looks like.

import json

from botocore.stub import Stubber


def test_send_to_s3():
    client = app.get_s3_client()
    stub = Stubber(client)
    stub.add_response(
        'put_object',
        expected_params={
            'Bucket': 'mybucket',
            'Key': 'myobject',
            'Body': b'{"hello": "world"}',
        },
        service_response={},
    )
    with stub:
        with Client(app.app) as client:
            response = client.http.put(
                '/resources/myobject',
                body=json.dumps({'hello': 'world'}).encode('utf-8')
            )
            assert response.status_code == 204
        stub.assert_no_pending_responses()

We can one again run these tests using pytest.

$ py.test tests/test_app.py
============================= test session starts ==============================
platform darwin -- Python 3.7.3, pytest-5.3.1, py-1.5.3, pluggy-0.12.0
rootdir: /tmp/testclient
plugins: hypothesis-4.43.1, cov-2.8.1
collected 5 items

test_app.py .....                                                         [100%]

============================= 5 passed in 0.43s ================================

Next Steps

For more information on using the test client in Chalice, you can check out our testing documentation as well as our API reference. Let us know what you think. You can share feedback with us on our GitHub repository.