Testing

Chalice provides a test client in chalice.test that you can use to test your Chalice applications. This client lets you invoke Lambda function and event handlers directly, as well as test your REST APIs.

Lambda Functions

To test lambda functions, use the Client.lambda_.invoke method. The test client is intended to be used as a context manager. For example, given this sample app:

from chalice import Chalice

app = Chalice(app_name="testclient")

@app.lambda_function()
def foo(event, context):
    return {'hello': 'world'}

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

Here’s how you can test these functions with the test client. In our example, we’ll be using pytest, but the Chalice test client will work with any testing framework. We’ll create a new tests/ directory and create a tests/__init__.py and a tests/test_app.py file.

$ mkdir tests
$ touch tests/{__init__.py,test_app.py}

The tests/test_app.py file should have the following contents:

from chalice.test import Client
from app import app

def test_foo_function():
    with Client(app) as client:
        result = client.lambda_.invoke('foo')
        assert result.payload == {'hello': 'world'}

def test_bar_function():
    with Client(app) as client:
        result = client.lambda_.invoke(
            'bar', {'my': 'event'})
        assert result.payload == {'event': {'my': 'event'}}

Now we can run our tests with 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 ============================

Note

See the Pytest Fixtures section for how to use pytest fixtures with the Chalice test client.

For testing Lambda functions that are connected to specific events, you can use the Client.events attribute to generate sample events. For example:

from chalice import Chalice

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

# Test code

from chalice.test import Client

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

Environment Variables

The Chalice test client will also configure any environment variables you have configured with your Lambda functions in your .chalice/config.json file. For example, suppose you had these config file:

{
    "version": "2.0",
    "app_name": "testenv",
    "stages": {
        "prod": {
            "api_gateway_stage": "api",
            "environment_variables": {
                "MY_ENV_VAR": "TOP LEVEL"
            },
            "lambda_functions": {
                "bar": {
                    "environment_variables": {
                        "MY_ENV_VAR": "OVERRIDE"
                    }
                }
            }
        }
    }
}

These sets a MY_ENV_VAR environment variable for the prod stage. The bar function overrides this environment variable with its own custom value. To test this, we need to specify the prod stage when we create our test client:

from chalice import Chalice

app = Chalice(app_name="testclient")

@app.lambda_function()
def foo(event, context):
    return {'value': os.environ.get('MY_ENV_VAR')}

@app.lambda_function()
def bar(event, context):
    return {'value': os.environ.get('MY_ENV_VAR')}

 # Test code
from chalice.test import Client

def test_foo_function():
    with Client(app, stage_name='prod') as client:
        result = client.lambda_.invoke('foo')
        assert result.payload == {'value': 'TOP LEVEL'}

def test_bar_function():
    with Client(app) as client:
        result = client.lambda_.invoke('bar')
        assert result.payload == {'value': 'OVERRIDE'}

REST APIs

You can test your REST API with the Chalice test client using the Client.http attribute. For example, given this REST API:

from chalice import Chalice

app = Chalice(app_name="testclient")

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

You can test this route with:

from chalice.test import Client
from app import app

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

If you want to access the response body’s raw bytes, you can use the body attribute:

from chalice.test import Client
from app import app

 def test_index():
     with Client(app) as client:
         response = client.http.get('/')
         assert response.body == b'{"hello":"world"}'

You can also test other HTTP methods by using the corresponding post(), put(), delete(), etc. method calls.

import json
from chalice import Chalice

app = Chalice(app_name="testclient")

@app.route('/', methods=['POST'])
def index()
    return app.current_request.json_body


def test_index():
   with Client(app) as client:
       response = client.http.post(
           '/myview',
           headers={'Content-Type':'application/json'},
           body=json.dumps({'example':'json'})
       )
       assert response.json_body == {'example': 'json'}

You can also test builtin authorizers with the test client:

from chalice import Chalice

app = Chalice(app_name="testclient")

@app.authorizer()
def myauth(event)
    if event.token == 'allow':
        return AuthResponse(['*'], principal_id='id')
    return AuthResponse([], principal_id='noone')

@app.route('/needs-auth', authorizer=myauth)
def needs_auth()
    return {'success': True}

#  Test code:
from chalice.test import Client

 def test_needs_auth():
     with Client(app) as client:
         response = client.http.get(
             '/needs-auth', headers={'Authorization': 'allow'})
         assert response.json_body == {'success': True}
         assert client.http.get(
             '/needs-auth',
             headers={'Authorization': 'deny'}).status_code == 403

Testing Boto3 Client Calls

If your event handlers are making AWS API calls using boto3 or botocore, you can use the botocore stubber to test your API calls. For example, suppose we have an app that makes an API call to Amazon Rekognition whenever an object is uploaded to S3:

import boto3

from chalice import Chalice

app = Chalice(app_name='testclient')
_REKOGNITION_CLIENT = None


def get_rekognition_client():
    global _REKOGNITION_CLIENT
    if _REKOGNITION_CLIENT is None:
        _REKOGNITION_CLIENT = boto3.client('rekognition')
    return _REKOGNITION_CLIENT


@app.on_s3_event(bucket='mybucket',
                 events=['s3:ObjectCreated:*'])
def handle_object_created(event):
    client = get_rekognition_client()
    response = client.detect_labels(
        Image={
            'S3Object': {
                'Bucket': event.bucket,
                'Name': event.key,
            },
        },
        MinConfidence=50.0
    )
    labels = [label['Name'] for label in response['Labels']]
    # In the real app we'd now do something with these labels
    # (e.g. store than in a database so we can query them later).
    return labels

To test this, we’ll combine the botocore stubber and the Chalice test client:

from chalice.test import Client
import app

from botocore.stub import Stubber

def test_calls_rekognition():
    client = app.get_rekognition_client()
    stub = Stubber(client)
    stub.add_response(
        'detect_labels',
        expected_params={
            'Image': {
                'S3Object': {
                    'Bucket': 'mybucket',
                    'Name': 'mykey',
                }
            },
            'MinConfidence': 50.0,
        },
        service_response={
            'Labels': [
                {'Name': 'Dog', 'Confidence': 75.0},
                {'Name': 'Mountain', 'Confidence': 80.0},
                {'Name': 'Snow', 'Confidence': 85.0},
            ]
        },
    )
    with stub:
        with Client(app.app) as client:
            event = client.events.generate_s3_event(
                bucket='mybucket', key='mykey')
            response = client.lambda_.invoke('handle_object_created', event)
            assert response.payload == ['Dog', 'Mountain', 'Snow']
        stub.assert_no_pending_responses()

In the testcase above, we first tell the stubber what API call we’re expecting, along with the parameters we’ll send and the response we expect back from the Rekognition service. Next we use the with stub: line to activate our stubs. This also ensures that when our test exits that we’ll deactive the stubs for this client. Now we the client.lambda_.invoke method is called, our stubbed client will return the preconfigured response data instead of making an actual API call to the Rekognition service.

Pytest Fixtures

Both the Botocore stubber and the Chalice test client are used within a context manager. In our previous example, this resulted in multiple levels of nesting, which is required for every test we write. If you’re using pytest as your test framework, you can create test fixtures to reduce the boiler plate code. Let’s rewrite several of these tests to use pytest fixtures.

First we’ll create a test fixture for the Chalice test client:

import app
from pytest import fixture
from chalice.test import Client

@fixture
def test_client():
    with Client(app.app) as client:
        yield client

Now our original tests for the foo and bar Lambda functions from the Lambda Functions section can be rewritten to use this fixture:

def test_foo_function(test_client):
    result = test_client.lambda_.invoke('foo')
    assert result.payload == {'hello': 'world'}

def test_bar_function(test_client):
    result = test_client.lambda_.invoke(
        'bar', {'my': 'event'})
    assert result.payload == {'event': {'my': 'event'}}

We can also create a fixture for the botocore stubber. This allows us to rewrite the test_calls_rekognition() test from the previous section in a more simplified manner. Below is the entire test file using both the botocore and Chalice test client fixtures:

import app
from pytest import fixture
from chalice.test import Client


@fixture
def test_client():
    with Client(app.app) as client:
        yield client


@fixture
def rekognition_stub():
    client = app.get_rekognition_client()
    stub = Stubber(client)
    with stub:
        yield stub


def test_calls_rekognition(test_client, rekognition_stub):
    rekognition_stub.add_response(
        'detect_labels',
        expected_params={
            'Image': {
                'S3Object': {
                    'Bucket': 'mybucket',
                    'Name': 'mykey',
                }
            },
            'MinConfidence': 50.0,
        },
        service_response={
            'Labels': [
                {'Name': 'Dog', 'Confidence': 75.0},
                {'Name': 'Mountain', 'Confidence': 80.0},
                {'Name': 'Snow', 'Confidence': 85.0},
            ]
        },
    )
    event = test_client.events.generate_s3_event(
        bucket='mybucket', key='mykey')
    response = test_client.lambda_.invoke('handle_object_created', event)
    assert response.payload == ['Dog', 'Mountain', 'Snow']
    stub.assert_no_pending_responses()

Next Steps

For reference documentation on the methods and attributes of the Chalice test client, see the test client section in the API documentation.

Middleware →