Testing boto3 code with moto and Stubber

boto3 · Practical · 12 min

Tests that hit real AWS are slow, flaky and cost money; mocks make AWS code testable in CI.

Two complementary tools:

moto (pip install moto) implements AWS services in memory. Decorate a test or use a context manager with mock_aws(), and every boto3 call inside goes to moto instead of AWS: you can create buckets, tables and queues and your code works against them with realistic behavior (errors, pagination, conditions). Set fake credentials (AWS_ACCESS_KEY_ID=testing and friends) and a region in a fixture so tests can never touch a real account. Create clients inside the mock, or at least after it starts.

botocore's Stubber works at the client level: you queue up expected calls with stubber.add_response("get_item", response_dict, expected_params) or add_client_error("get_item", "ResourceNotFoundException"). It asserts that your code makes exactly those calls with exactly those parameters and returns your canned responses; assert_no_pending_responses() checks that all expected calls happened. It's precise but brittle, and responses must match the service's response shape.

Use moto for most tests of AWS-facing logic, Stubber when you must assert exact parameters or simulate rare errors. For integration tests closer to reality, LocalStack runs AWS APIs in a container, reachable with endpoint_url=.

Whatever you choose, keep AWS calls behind small functions that take a client parameter: that makes injection trivial.

Example

# test_storage.py   (pip install "moto[s3]" pytest)
import boto3, pytest
from botocore.stub import Stubber
from moto import mock_aws
def save_report(s3, bucket, name, text):
    s3.put_object(Bucket=bucket, Key=f"reports/{name}", Body=text.encode())
    return f"s3://{bucket}/reports/{name}"
@pytest.fixture
def aws_env(monkeypatch):
    for k in ("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"):
        monkeypatch.setenv(k, "testing")
    monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")

@mock_aws
def test_save_report_with_moto(aws_env):
    s3 = boto3.client("s3"); s3.create_bucket(Bucket="acme-reports")
    assert save_report(s3, "acme-reports", "q1.txt", "hi") == "s3://acme-reports/reports/q1.txt"
    assert s3.get_object(Bucket="acme-reports", Key="reports/q1.txt")["Body"].read() == b"hi"

def test_save_report_with_stubber(aws_env):
    s3 = boto3.client("s3")
    with Stubber(s3) as stub:
        stub.add_response("put_object", {}, {"Bucket": "acme-reports", "Key": "reports/x", "Body": b"y"})
        save_report(s3, "acme-reports", "x", "y")
        stub.assert_no_pending_responses()

Remember

  • moto's mock_aws runs AWS services in memory for realistic tests
  • Always set fake credentials and a region in test fixtures
  • Stubber asserts exact calls and returns canned responses
  • Pass clients into functions so tests can inject them

Watch out for

  • Creating a module-level client before mock_aws starts, so tests talk to real AWS.

Check yourself

  1. Which tool lets you assert the exact parameters your code sends to put_object?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: botocore.amazonaws.com/v1/documentation/api/latest/reference/stubber.html