---
title: "Testing AWS Chalice Applications"
description: "A tutorial to learn how to write unit tests and integration tests for REST APIs in AWS Chalice. Additionally, we will see how to measure test coverage."
authors:
  - name: "Jekayinoluwa Olabemiwo"
    url: "https://auth0.com/blog/authors/jekayinoluwa-olabemiwo/"
date: "Sep 6, 2021"
category: "Developers,Tutorial,AWS"
tags: ["aws", "chalice", "testing"]
url: "https://auth0.com/blog/testing-aws-chalice-applications/"
---

# Testing AWS Chalice Applications

**TL;DR:** In this tutorial, we will learn how to write unit tests and integration tests with Pytest in AWS Chalice applications. We will also learn how to measure test coverage.

## Introduction

[AWS Chalice](https://aws.github.io/chalice/) is a Python-based web micro-framework that leverages on the [AWS Lambda](https://aws.amazon.com/lambda/) and [API Gateway](https://aws.amazon.com/api-gateway/) services. It is used to create serverless applications. The Chalice experience is [Flask](https://flask.palletsprojects.com/en/2.0.x/)-like by way of features like semantics and syntax. For more details on creating and deploying Chalice applications, you can go through the article on [how to create CRUD REST API with AWS Chalice](https://auth0.com/blog/how-to-create-crud-rest-api-with-aws-chalice/).

When building applications, there is a need to [test](https://en.wikipedia.org/wiki/Software_testing) the code to avoid shipping bugs and unstable code. It also saves one a lot of debugging hours and makes deployments less stressful.

Common forms of tests in software development include:

1. [Unit test](https://en.wikipedia.org/wiki/Unit_testing): This tests a particular function, component, or logic in the code. This way, edge cases can easily be identified, isolated, and fixed. Unit tests usually involve inspecting the output of a function against a known or expected output.
2. [Integration Test](https://en.wikipedia.org/wiki/Integration_testing): examines multiple parts or the entire application in an end-to-end manner. It considers how each function or component works with the other.

However, Chalice currently provides a test client for just unit tests. Therefore, the integration tests are written in a manner similar to unit tests with no major difference other than the fact that integration tests consist of multiple unit tests.

## Where to Write Tests

In Python-based applications, tests are usually housed in `test.py` files. These are the test files that will import the application logic to be tested.

Let's assume we have a simple Chalice application with a folder structure that looks like the following:

```bash
├── app.py
├── .chalice
├── requirements.txt
└── test.py
```

However, as the application becomes bigger, a single `test.py` file would become bulky and can become difficult to work with. Hence, it is necessary to create a folder of tests called `tests` and then split the tests into multiple test files inside the test folder.

Now, we'll need to create a new folder called `tests` and an empty `__init__.py` file inside the folder. The `__init__.py` will allow Python to recognize the test directory as a package that could be run:

```bash
mkdir tests && cd tests
touch __init__.py
```

Now, let's create a test file for our application inside the test folder:

```bash
touch test_unit.py
```

Then, the folder structure will look like this:

```bash
├── app.py
├── .chalice
├── requirements.txt
└── tests
    ├── __init__.py
    └── test_unit.py
```

We can then create as many test files as we need in the `tests` folder.

## How to Do Unit Tests

Since the release of v.1.17.0, AWS Chalice has shipped with a test client that serves as a test runner to write tests in Chalice applications. We no longer need to set up boilerplates and logic for testing. We only need to import the test client into our test file.

Let's assume we have our Chalice application has an `app.py` file that looks like the following:

```python
from chalice import Chalice

app = Chalice(app_name='chalice-api-sample')


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

Now, we'll modify the `test_unit.py` file as follows:

```python
import app
from chalice.test import Client
```

In the above code, we have just imported the `app.py` and the chalice test Client. Let's add the following test code:

```python
...

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

In the above test code:

1. we instantiated the test Client to be used within the context of the particular test function. That implies that whenever we run the test, a test environment with resources and environment variables will be set up and then cleaned up after running the test.
2. We made a `GET` request via HTTP using the `client.http` attribute.
3. We assert that a `200` status code is returned in the response with a JSON response body—`{'hello': 'world'}`.

We will install [Pytest](https://docs.pytest.org/en/6.2.x/getting-started.html) runner to run our test:

```bash
pip install pytest
```

Then, we will run with the following command:

```bash
py.test tests/test_unit.py
```

We should get a response that looks like the following:

```bash
======================================== test session starts ========================================
platform win32 -- Python 3.7.7, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: C:\aws-chalice-api-sample
collected 1 item

tests\test_unit.py .                                                                           [100%]

========================================= 1 passed in 0.11s =========================================
```

### Creating mocks

We can try to [mock](https://en.wikipedia.org/wiki/Mock_object) an external API in order to learn how to [test with mock variables](https://docs.python.org/3/library/unittest.mock.html). We will test a request from our app to an endpoint of [JSONPlaceholder](https://jsonplaceholder.typicode.com/) API data service to get a list of dummy posts. Firstly, let us install the Python requests module:

```bash
pip install requests
```

Then, let's add a function inside `app.py` file to make a `GET` request to the `/post` endpoint of JSONPlaceholder and return a list of posts:

```python
...
import requests

@app.route('/post')
def get_post():
    response = requests.get("https://jsonplaceholder.typicode.com/posts/1")
    if response.ok:
        return response.json()
    else:
        return None
...
```

In the above code, we wrote a function called `get_post` that makes an HTTP request to the DummyAPI server and returns the response of the request in JSON form.

So, we'll add a mock inside the `test_unit.py` file:

```python
...
from unittest.mock import patch

...

@patch('app.requests.get')
def test_get_post(mock_get):
    """Mocking with the patch decorator to get a post from an External API"""
    mock_get.return_value.ok = True
    response = app.get_post()
    assert response.ok

```

In the above code, we imported the patch function of the mock module. Then, we defined the patch function as a decorator with reference to the project's `request.get`. Then, we created a function called `test_get_post` with a parameter called `mock_get` to test the `get_post` function inside the `app.py` file. If the returned status of `mock_get` is `ok`, then a fake request to the JSONPlaceholder is made, after which an assertion of the status code of the response to the request is made. We ensured that the mock acts like it is making a real `request.get` request to the JSONPlaceholder server, whereas it is a fake request. This allows us to test our code without dependence on the JSONPlaceholder external API server. 

If we run our test again with the following bash command:

```bash
py.test tests/test_unit.py
```

We should get an output similar to the following:

```bash
========================================= test session starts =======================================
platform win32 -- Python 3.7.7, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: C:\aws-chalice-api-sample
plugins: cov-2.12.1
collected 2 items

tests\unit\test_unit.py ..                                                                     [100%]

========================================= 2 passed in 0.85s =========================================
```

How can we test when the desired response is not returned? That's why we have an `else` statement in the `get_post()` function inside the `app.py` file, right? To accommodate situations when no post is returned when we request for a post from JSONPlaceholder. Therefore, we will add a test that will check when no post is returned. Let's add the following code in the `test_unit.py` file:

```python
...
@patch('app.requests.get')
def test_no_get_post(mock_get):
    """Mock testing to check when no post is returned"""
    mock_get.return_value.ok = False
    response = app.get_post()
    assert response == None
```

In the above file, we asked the test to check if the returned value of the mock `GET` request is not `ok` in the line `mock_get.return_value.ok = False`. We also asserted that a `None` response is returned. So, we have been able to handle situations where a post is not returned.

We can then run the `test_unit.py` file again as thus:

```bash
py.test tests/test_unit.py
```

We will get the following output:

```bash
========================================= test session starts ======================================= 
platform win32 -- Python 3.7.7, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: \aws-chalice-api-sample
plugins: cov-2.12.1
collected 3 items

tests\unit\test_unit.py ...                                                                    [100%] 

========================================= 3 passed in 0.17s ========================================= 
```

Cool!

The full code of the `test_unit.py` file is thus:

```python
from unittest.mock import patch
import app
from chalice.test import Client
import json


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

@patch('app.requests.get')
def test_get_post(mock_get):
    """Mocking with the patch decorator to get a post from an External API"""
    mock_get.return_value.ok = True
    response = app.get_post()
    assert response.ok

@patch('app.requests.get')
def test_no_get_post(mock_get):
    """Mock testing to check when no post is returned"""
    mock_get.return_value.ok = False
    response = app.get_post()
    assert response == None

```


## How to Write Integration Tests

Integration tests check multiple components to see if they work together. These tests are usually written like unit tests, but they involve verifying multiple parts of the application at once. An integration test might require establishing a network connection, setting up a database, etc. These can be configured as fixtures. Fixtures are functions that set up the initial states/environment that you can create in your tests once and use multiple times.

It is a good habit to separate unit tests from integration tests by putting them in separate folders. Hence, we will create two folders inside the `test` directory and call them `unit` and `integration`, respectively. Then,

- we will move the `test_unit.py` file into the `unit` folder.
- also, we will create a new `test_integration.py` file in the `integration` folder
- next, we will create a `conftest.py` file in the `test` folder to house our fixtures.

So, the folder structure will look like the following:

```bash
├── app.py
├── .chalice
├── requirements.txt
└── tests
    ├── __init__.py
    ├── conftest.py
    ├── unit/
    │   ├── __init__.py
    │   └── test_unit.py
    │
    └── integration/
        ├── __init__.py
        └── test_integration.py
```

To start with, we will create a fixture called `app`. This fixture will be the instance of our Chalice application. Let's go to the `conftest.py` file and change it thus:

```python
import pytest
from chalice import Chalice
import app as chalice_app
from chalice.test import Client


@pytest.fixture
def app() -> Chalice:
    return chalice_app


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

In the code above, we've abstracted an instance of our Chalice application as a fixture. We also created a fixture for our test client.

Going forward, let's assume that we have set up a REST API for a bookshelf application in the `app.py` with CRUD endpoints like the following:

```python
...

# POST endpoint to add books to the bookshelf

@app.route('/book', methods=['POST'])
def create_book():
    book_as_json = app.current_request.json_body
    try:
        Item = {
            'id': book_as_json['id'],
            "title": book_as_json['title'],
            "author": book_as_json['author']
        }
        return {"id": book_as_json['id'], "title": book_as_json['title'], "author": book_as_json['author']}
    except Exception as e:
        return {'message': str(e)}


# PUT endpoint to update a book item based on the given ID

@app.route('/book/{id}', methods=['PUT'])
def update_book(id):
    book_as_json = app.current_request.json_body
    try:
        Item = {
            "id": book_as_json['id'],
            "title": book_as_json['title'],
        }
        return {'message': 'ok - UPDATED', 'status': 201}
    except Exception as e:
        return {'message': str(e)}


# DELETE endpoint to delete a particular book based on the given ID

@app.route('/book/{id}', methods=['DELETE'])
def delete_book(id):
    book_as_json = app.current_request.json_body
    try:
        Item = {
            "id": book_as_json['id'],
            "author": book_as_json['author']
        }
        return {'message': 'ok - DELETED', 'status': 201}
    except Exception as e:
        return {'message': str(e)}

```

The code above consists of:

- `add_book()`: function for `POST` method to add books to the catalog
- `update_book(id`: uses the `UPDATE` method to update a specified book entry with a new title
- `delete_book(id)`: deletes a particular book entry from the catalog

Now, we can write tests for them using the Chalice [TestHTTPClient class](https://aws.github.io/chalice/api.html#Client.http) inside the `test_integration.py` file like the following:

```python
import json

#  test for the create_book endpoint
def test_add_book(test_client):
    response = test_client.http.post(
        '/book',
        headers={'Content-Type': 'application/json'},
        body=json.dumps(
            {
                "id": "123",
                "title": "Javascript Know It All",
                "author": "Chukwuma Obinna",
            })
    )
    assert response.json_body == {
        "id": "123",
        "title": "Javascript Know It All",
        "author": "Chukwuma Obinna"
    }


#  test for the update_book endpoint
def test_update_book(test_client):
    response = test_client.http.put(
        '/book/{id}',
        headers={'Content-Type': 'application/json'},
        body=json.dumps(
            {
                "id": "123",
                "title": "Chalice Book",
            })
    )
    assert response.json_body == {
        "message": "ok - UPDATED",
        "status": 201
    }


#  test for the delete_book endpoint
def test_delete_book(test_client):
    response = test_client.http.delete('/book/{id}',
         headers={'Content-Type': 'application/json'},
         body=json.dumps(
              {
                    "id": "123",
                    "author": "Chukwuma Obinna",
              })
    )
    assert response.json_body == {
        "message": "ok - DELETED",
        "status": 201
    }

```

In the above code:

- We wrote the tests for each of our CRUD endpoints.
- In each test function, we used the `test_client` that we defined as a fixture earlier on in the `conftest.py` file.
- defined the header and body to be passed in each test request
- We asserted known responses to the test requests

Note: the database functionality is not included in the API used in order to simplify the example. Otherwise, we would have to write a fixture to setup a mock database for the integration test.

To run the above test, we'd use the following command:

```bash
py.test tests/test_integration.py
```

We should get a response that looks like the following:

```bash
========================================= test session starts ======================================
platform win32 -- Python 3.7.7, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
cachedir: .pytest_cache
rootdir: C:\aws-chalice-api-sample
collected 4 items

tests/integration/test_integration.py::test_index PASSED                                     [ 25%]
tests/integration/test_integration.py::test_add_book PASSED                                  [ 50%]
tests/integration/test_integration.py::test_update_book PASSED                               [ 75%]
tests/integration/test_integration.py::test_delete_book PASSED                               [100%]

========================================= 4 passed in 1.01s =======================================
```

Generally, integration tests usually take a longer time to run than unit tests. Therefore, it is advisable to not run them every time but whenever one needs to deploy.

The full code of the `app.py` file is thus:

```python
from requests.models import Response
from chalice import Chalice
import requests
import json

app = Chalice(app_name='aws-chalice-api-sample')


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

# Function to make External API Call
@app.route('/post')
def get_post():
    response = requests.get("https://jsonplaceholder.typicode.com/posts/1")
    if response.ok:
        return response.json()
    else:
        return None

# Function to make POST request to create a book
@ app.route('/book', methods=['POST'])
def create_book():

    book_as_json = app.current_request.json_body
    try:
        Item = {
            'id': book_as_json['id'],
            "title": book_as_json['title'],
            "author": book_as_json['author']
        }
        return {"id": book_as_json['id'], "title": book_as_json['title'], "author": book_as_json['author']}
    except Exception as e:
        return {'message': str(e)}

# Function to make POST request to update a book
@app.route('/book/{id}', methods=['PUT'])
def update_book(id):
    book_as_json = app.current_request.json_body
    try:
        Item = {
            "id": book_as_json['id'],
            "title": book_as_json['title'],
        }
        return {'message': 'ok - UPDATED', 'status': 201}
    except Exception as e:
        return {'message': str(e)}

# Function to make POST request to delete a particular book a book
@app.route('/book/{id}', methods=['DELETE'])
def delete_book(id):
    book_as_json = app.current_request.json_body
    try:
        Item = {
            "id": book_as_json['id'],
            "author": book_as_json['author']
        }
        return {'message': 'ok - DELETED', 'status': 201}
    except Exception as e:
        return {'message': str(e)}

```

## Measuring Code Coverage

[Code coverage](https://en.wikipedia.org/wiki/Code_coverage) is simply a means of quantifying how much of our code is being tested. We will use [pytest-cov](https://pypi.org/project/pytest-cov/) package to measure test coverage in this tutorial. It is a tool built off the [coverage.py](https://coverage.readthedocs.io/en/coverage-5.5/) tool used in measuring coverage in Python code. Fortunately, pytest-cov works well with pytest.

Let's install pytest-cov:

```bash
pip install pytest-cov
```

Let's measure the test coverage by using the `-cov` argument with pytest. We will measure the coverage over our source code in the `app.py` file:

```bash
pytest --cov=app  --cov-report term-missing 
```
We used the `--cov-report` `term-missing` command to specify that we want our coverage report to indicate the lines of code that are not covered by our tests.

We will get a terminal output that looks like this:

```bash
======================================= test session starts =======================================
platform win32 -- Python 3.7.7, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: \aws-chalice-api-sample
plugins: cov-2.12.1
collected 7 items

tests\integration\test_integration.py ....                                                    [ 57%] 
tests\unit\test_unit.py ...                                                                   [100%] 

----------- coverage: platform win32, python 3.7.7-final-0 -----------
Name     Stmts   Miss  Cover   Missing
--------------------------------------
app.py      33      6    82%   46-47, 59-60, 72-73
--------------------------------------
TOTAL       33      6    82%

======================================== 7 passed in 2.16s ========================================

```

Note: The 6 missed exception statements in lines 46-47, 59-60, and 72-73 above are in the `app.py` file. The missing seemed to occur while running as a result of a pytest [error](https://github.com/nedbat/coveragepy/issues/605).

## Conclusion
 
In this article, we have considered how to run unit and integration tests in Chalice applications and APIs. We also learned how to use Pytest for testing and Pytest-cov for measuring code coverage. We can now go ahead and build [test-driven](https://developer.ibm.com/devpractices/software-development/articles/5-steps-of-test-driven-development/) Chalice applications with the knowledge gained.
Thanks for following through. We'd be glad to have your thoughts and suggestions in the comment section. Thanks.