Testing AWS Python code with moto

No Comments

In this article I want to share with you how moto hooks into boto3 and how you can use it to test existing Python code which interacts with your AWS infrastructure.
Recently I have been in a project in which we were working on machine learning pipelines within an AWS architecture. When I joined the project some of the code was already present, but untested. I did not feel comfortable to change and deploy it. That is why I started with setting up tests to better understand what is going on and to be able to change the code in a controlled fashion.

Why moto is a good fit

The code I wanted to test was written in Python and running on AWS infrastructure within Docker containers. Interaction with the AWS infrastructure was implemented with boto3, the AWS SDK for Python. The initialization of the aws-access happened somewhere within the functions I wanted to test.

Moto is a good fit in such a case, because it enables you to mock out all calls to AWS made in your code automatically.
There is no need for dependency injection. You can take the code you have in place and test it straight away. You just need to enable motos mocking functionality and take some precautions, that is it.

In the following I will show how to enable moto and what precautions to take.

How does moto actually mock things?

The main hook for moto into boto3 is a global list of handlers (named BUILTIN_HANDLERS) in botocore, which is the foundation of boto3.
All handlers within this global list are registered every time a session is instantiated. Once an internal event is emitted, the handlers registered for that kind of event are called.

A before-send-handler for example is executed just before an actual http-request is being send to AWS. If a before-send-handler returns a response, it is used for further processing and the http-request itself is skipped.

This is an example on how to skip http-requests in botocore by returning mock-responses in a custom before-send-handler:

from botocore.awsrequest import AWSResponse
from botocore.handlers import BUILTIN_HANDLERS
from moto.core.models import MockRawResponse
 
def my_custom_handler(request, event_name, **kwargs):
    # event_name is sth like 'before-send.s3.GetObject'
    return AWSResponse('',200,{},MockRawResponse(''))
 
BUILTIN_HANDLERS.append(("before-send", my_custom_handler))

Moto appends such a before-send-handler to the BUILTIN_HANDLERS. The handler is used to return mock-responses from moto mock backends we register. It is appended implicitly, when importing moto in your test code, but does not return (mock) anything by default. Mocking can be achieved by using moto-decorators (or any of the other initializations of moto), which are available for most of the AWS resources.

The moto-decorator registers a mock backend for the scope of the test function. The mock backend is used by the appended before-send-handler to return mock responses. Keep in mind, that the moto-decorator enables the mocking only for the scope of your test function. After your test passes, mock backends and testing credentials are being reset by moto.

Setting up moto and protecting your AWS environment

Interesting, so what happens if I import moto in my test code, but forget to use the moto decorator?
A missing moto decorator might cause changes in your AWS environment if you did not take precautions. Even though moto might have registered the handler in time implicitly, the mock backend would not be initialized and your code would try to access AWS.

That is why you should stick to the recommended usage of moto:

  • run your tests with testing-credentials
  • set up mocking before any usage/import of boto3

If you fail on setting up the mocks correctly, the testing credentials will still protect you from changing things in your AWS environment.

To be able to properly secure our test setup and provide test credentials to protect our AWS environment, we need to understand how boto3 accesses credentials.

Every time you instantiate a client with boto3, boto3 tries to find credentials to use for accessing AWS. You can see that if you activate the debug-logs for botocore.credentials or you can have a look in the documentation here.

import logging
import boto3
 
boto3.set_stream_logger('botocore.credentials', logging.DEBUG)
client = boto3.client('s3')

Would print something like:

[DEBUG] Looking for credentials via: env
[DEBUG] Looking for credentials via: assume-role
[DEBUG] Looking for credentials via: assume-role-with-web-identity
[DEBUG] Looking for credentials via: shared-credentials-file
[INFO] Found credentials in shared credentials file: ~/.aws/credentials

Boto3 looks at parameters passed during initialization in your code first. Secondly it checks if AWS environment variables have been set. Afterwards credentials, config files, … are being parsed.

Now after we know how moto hooks into boto3 and how credentials are being read, these are some precautions steps which should help us to set up a safe test environment which won’t interact with our AWS environment:

1. No parameters when initializing AWS access within the code

Passing no parameters to boto3 when initializing AWS access makes sure we can influence the connection from outside. For example by setting environment variables.

client = boto3.client('s3')

2. Initialize AWS access within a function

It is crucial to be able to control when exactly the AWS access is initialized and boto3 looks for credentials. Especially because the moto import in our test file implicitly appends a mock handler in botocore, which should be picked up by the boto3 initialization. The easiest way to do this is to have the initialization within a function in your scripts/applications code.

import boto3
 
def start():
    client = boto3.client('s3')

If you have it outside of a function and import your code in your test file, the import statement alone would cause the access to be initialized due to Pythons module loading. The ordering of your import statements would determine whether or not motos before-send-handler got registered within your client. That is why moto recommends to use inline imports when you have not wrapped your initialization in a function.

3. Initialize moto backends before you call your code

Once we made sure our initialization picked up the moto handler, we still need to register and activate mock backends in moto. If we don’t, our handler would not return mock responses and the requests might reach AWS. Moto initialization can be done in different ways. I prefer the decorator which is quite convenient.
The decorator adds and activates a mock backend for our test function, in this example a s3-backend.

from moto import mock_s3
from unittest import TestCase
 
class TestS3(TestCase):
 
    @mock_s3
    def test_s3(self):
        run_s3_code()

Keep in mind that calls to other backends could still reach AWS if you do not initialize the moto mocks.

4. Set testing credentials before you run your tests

It is still possible that you forget about your decorator or initialize a client on top level within your code, which would not pick up the moto handler. Because all this might happen it makes sense to be careful and take additional precautions.

Once you are in a moto testing block (for example test function with moto decorator), you are pretty safe. Moto itself sets testing credentials in the environment if you have at least one mock backend initialized. These credentials are being read by all boto3 initializations within your test code.

It is still a good idea to have some setup code or fixture (if you use pytest), which sets invalid AWS credentials before the test starts to run. This helps you even if you forget about the moto-decorator. If you have module code which gets executed on import, this still might not protect you. Then again it depends on what comes first. The initialization of the client or the AWS-credentials in the environment.

As a solution I personally prefer a layered approach:

  • testing credentials within your code
  • testing credentials within the default-profile of your ~/.aws/credentials file (fallback)

Every time I execute some code accidentally, forget to initialize moto or anything else, boto3 in worst case would fallback to my credentials file at some point and pick up these invalid testing credentials. In cases where I really want to access AWS I work with profiles.

[default]
aws_access_key_id=testing
aws_secret_access_key=testing

[codecentric]
aws_access_key_id=some_id
aws_secret_access_key=some_key

...

Testing credentials can of course also be set in bash scripts which execute tests, IDE run configurations, pipelines, …

Example

Knowing all this, lets look at a specific case and see if we understand what is going on.

import boto3
 
client = boto3.client('s3') # done outside the method for demonstration purpose
 
def run_s3_code():
    buckets = client.list_buckets()['Buckets']

What is going on when we run the test? Would it access AWS or not and why?

from moto import mock_s3
from s3.s3_code import run_s3_code
from unittest import TestCase
 
class TestS3(TestCase):
 
@mock_s3
def test_s3(self):
    run_s3_code()

The answer is, it would not access AWS. So what is going on here?

1. moto before-send-handler

from moto import mock_s3

The very first line of our code is the moto import.
This line implicitly adds the before-send-handler within botocore which would not return anything yet.

2. boto3 client initialization

from s3.s3_code import run_s3_code

The second line causes the initialization of our client which we did not wrap in a function. On initialization the global handlers are registered, also the before-send-handler which is already present (1.).

3. adding moto backends

@mock_s3
def test_s3(self):

The decorator adds and initializes a mock-backend which will be used by the before-send-handler. All code we execute within the test function now which uses our client to access s3, would reach the mock-backend instead of AWS.


So what happens if we change the order of imports in a way that

from moto import mock_s3
from s3.s3_code import run_s3_code

becomes

from s3.s3_code import run_s3_code
from moto import mock_s3

Right. We would access our AWS environment when running the test. Our client would get initialized first and would not register motos before-send-handler. Be careful here and stick to the recommendations: Initialize your AWS access within a function.

Testing code with mocked s3 interaction

Once we setup and secured our testing setup, we can start to write tests.
A simple test could look something like this:

import json
from unittest import TestCase
import boto3
from moto import mock_s3
from s3.s3_code import run_s3_code
 
class TestS3(TestCase):
 
    @mock_s3
    def test_s3(self):
        bucket = 'test-bucket'
        s3_client = boto3.client('s3')
        s3_client.create_bucket(Bucket=bucket)
 
        # GIVEN
        s3_client.put_object(Bucket=bucket,
                             Key="market_a/market_infos.json",
                             Body=json.dumps({
                                 'products': [
                                     {'price': 150},
                                     {'price': 250},
                                 ]
                             }))
 
        # WHEN
        run_s3_code()
 
        # THEN
        result = int(s3_client.get_object(Bucket=bucket,
                                          Key="market_a/total_price.txt")['Body'].read())
        self.assertEqual(result, 400)

The tested code:

import json
 
import boto3
 
def run_s3_code():
    client = boto3.client('s3')
 
    market_infos = client.get_object(
        Bucket="test-bucket",
        Key="market_a/market_infos.json"
    )['Body'].read().decode('utf-8')
 
    client.put_object(
        Bucket="test-bucket",
        Key="market_a/total_price.txt",
        Body=str(sum(map(lambda a: a['price'], json.loads(market_infos)['products'])))
    )

Summing up

When testing your code which interacts with AWS using moto, you want to make sure that there is no real interaction with AWS. In order to do that you should

  • put your initialization into functions
  • initialize moto mocking properly
  • setup testing-credentials in your code and your local default-profile

Moto has been around for quite some time and is a very convenient library to mock AWS services. If you don’t use Python it also has a stand-alone server mode which you can use.

Are you using moto? What are your experiences?

Kai Brandes

Kai is an experienced software developer. He is convinced of agile practices and, as a former test manager, gives high priority to software quality. His main focus lies on the JVM languages Java, Scala and Clojure which he uses together with frameworks like Akka and Spark to build Microservice and Big Data architectures.

Comment

Your email address will not be published. Required fields are marked *