[proposal] Chalice Test Client
See original GitHub issueTest Client Proposal
This proposal outlines improvements to Chalice that enable users to easily test their applications.
Goals
- Add new Chalice APIs for easily testing a Chalice application.
- Must support not only REST APIs, but all other Chalice event handlers. Even if all event handlers are not implemented initially, the design of the APIs should be finished.
- We want the API to feel familiar to python developers. While we will need to adapt to Lambda, we don’t want to stray too far from existing testing APIs that developers use in other frameworks. This API should not have a steep learning curve.
Non Goals
Part of successfully testing an app requires the ability to swap out objects with mock/fake versions. This is out of scope for this proposal, but is addressed in a separate follow up proposal. This proposal focuses on new testing APIs through clients that allow you to invoke your lambda handlers and rest/websocket APIs.
Prior Work
-
Test client for Chalice rest APIs: https://github.com/aws/chalice/issues/1120 Corresponding PR: https://github.com/aws/chalice/pull/1193
-
Tracking github issue for testing: https://github.com/aws/chalice/issues/289 “Sample for writing tests for Chalice app”
The proposal mostly builds off that work and generalizes it to include testing for all of Chalice’event handlers.
Flask
Docs for testing flask apps: https://flask.palletsprojects.com/en/1.1.x/testing/
Flask lets you test through a context manager, the basic test looks like:
def test_foo():
with mymodule.app.test_client() as client:
rv = client.get('/')
assert b'my test' in rv.data
They also have some documentation on how to use this with pytest. There’s also a more low level context manager that lets you set the context:
import flask
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
assert flask.request.path == '/'
assert flask.request.args['name'] == 'Peter'
Django
Django also has a test client:
>>> from django.test import Client
>>> c = Client()
>>> response = c.post('/login/', {'username': 'john', 'password': 'smith'})
>>> response.status_code
200
>>> response = c.get('/customer/details/')
>>> response.content
b'<!DOCTYPE html...'
Specification
Most of the frameworks I looked at have a dedicated module for testing:
- Flask and Sanic includes the
test_client
directly on the app, along with a corresponding test module. - Django has a
django.test
client. - FastAPI has a
fastapi.testclient
.
Given we’re careful about what goes into the Lambda deployment package
(and therefore the app.py file),
I think we should have a separate chalice.test
module, but I think
we shouldn’t add anything on to the app object itself. The Chalice
app object
should remain as minimal as possible. This means usage will require you
to import the test module and instantiate a client by providing your own
app object.
Proposed Testing API
There will be a Client
class that accepts a chalice.Chalice
object as
input. It will then have various attributes and methods you can invoke to test
your app. There’s a separate events
, http
, ws
, and lambda_
(unfortunate name I know) attributes that allow you to test the different type
of event handlers. For lambda_
testing, the main test method is invoke
.
It accepts the function name (not the fully qualified name though with the
app and stage prefixes) as input. Examples:
Testing a pure lambda function:
app = Chalice('myapp')
@app.lambda_function()
def myfunction(event, context):
return {'event': event}
with chalice.test.Client(app.app) as client:
result = client.lambda_.invoke('myfunction', {'hello': 'world'})
assert result.body == {'event': {'hello': 'world'}}
Testing an event handler:
@app.on_sns_message(topic='mytopic')
def handle_sns_message(event):
return json.loads(event).get('JobId')
with chalice.test.Client(app) as client:
result = client.lambda_.invoke(
'handle_sns_message',
client.events.generate_sns_event(message='{"JobId": "myid"}',
subject='bar'))
# A future enhancement might allow you to specify emitting an
# event and Chalice figuring out what handles to invoke.
client.events.emit_sns_event(
topic='mytopic', message='foo', subject='bar')
The http
client mimics the flask/django/requests API with methods
mapping to HTTP methods. They return a response similar to request’s
Response class:
Testing a REST API:
@app.route('/')
def index():
return {'hello': 'world'}
@app.route('/name/{name}')
def hello(name):
return {'hello': name}
with chalice.test.Client(app) as client:
response = client.http.get('/')
assert response.status_code == 200
assert response.json_body == {'hello': 'world'}
Internally this uses the LocalGateway
, so it supports everything
that local mode supports and we can ensure we always have feature
parity between those local mode and this test client.
The test client also honors your chalice config so you can test env vars and
various configuration set for a lambda function. For example, given
a .chalice/config.json
file of:
{
"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"
}
}
}
}
}
}
and an app of:
@app.lambda_function()
def foo(event, context):
return {'myvalue': os.environ.get('MY_ENV_VAR')}
@app.lambda_function()
def bar(event, context):
return {'myvalue': os.environ.get('MY_ENV_VAR')}
You’d get these results:
def test_env_vars():
with Client(app, stage_name='prod') as client:
assert client.lambda_.invoke('foo', {}).payload == {
'myvalue': 'TOP LEVEL'
}
assert client.lambda_.invoke('bar', {}).payload == {
'myvalue': 'OVERRIDE'
}
And lastly, testing a websocket handler involves emulating
the connection tracking so that the app.websocket_api.send
does what you’d expect.
Testing a websocket handler:
@app.on_ws_message()
def on_message(event):
app.websocket_api.send(connection_id=event.connection_id,
message=event.body)
with chalice.test.Client(app) as client:
conn1 = client.ws.new_connection(connection_id='12345')
# connection_id is optional
conn2 = client.ws.new_connection()
conn1.send('hello world')
conn2.send('hello world conn2')
conn2.send('hello world conn2 again')
assert conn1.recv() == 'hello world'
assert conn1.recv() is None
assert conn2.recv() == 'hello world conn2'
assert conn2.recv() == 'hello world conn2 again'
assert conn2.recv() is None
Issue Analytics
- State:
- Created 3 years ago
- Reactions:10
- Comments:6 (1 by maintainers)
this new testing feature really makes a difference. I love Chalice, and probably it is one of the very few things tying me to AWS, but debugging/performing CI/CD tests in pure lambdas and SQS-triggered lambdas required some nasty workarounds that didn’t cover all use cases. I’ve been trying this in the new version and you just gave me more reasons to love this package. Just wanted to comment to say thanks!
May I propose the ability to set a custom context on the
LocalGateway
? This would make it easy to test endpoints which require Cognito. In pytest-chalice, it’s possible like this:There was some discussion on implementing it directly in Chalice here.