Mocking AWS Services with moto in pytest
moto replaces the AWS API in-process so the boto3 calls your IaC helper code makes return realistic responses without a network round-trip or real credentials. This task sits within Testing Python Infrastructure Code, part of the wider Python IaC fundamentals and strategy toolkit, and it is the right tool whenever your infrastructure code drops to the AWS SDK for a lookup or an imperative step.
Context
Pulumi and CDKTF cover most resource provisioning, but real projects still reach for boto3 to look up an existing VPC, resolve the latest AMI, or read a parameter before building the resource graph. That SDK code needs the same test coverage as the rest of your infrastructure. moto intercepts boto3 at the client level and serves an in-memory AWS, so you can assert what your helper does with the response and even inspect the calls it made — all in milliseconds and with no live account.
Prerequisites
- Python 3.9+ and
pytest>=7. pip install "moto[ec2,s3]>=5"(moto 5 consolidated decorators intomock_aws).boto3>=1.26as a direct dependency of your IaC helper package.- Dummy AWS credentials set in the test environment so boto3's credential chain does not error before moto intercepts.
- No real IAM permissions — moto never contacts AWS.
Implementation
1. Pin fake credentials and a region in a fixture
boto3 resolves credentials and region before any call, so set throwaway values to keep the chain from reaching a real profile.
# CLI: pytest tests/test_lookups.py -v
# Provider note: these creds are never sent anywhere; moto answers locally.
import os
import pytest
@pytest.fixture(autouse=True)
def aws_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
2. Wrap the test body in mock_aws and seed state
The single mock_aws decorator covers every service. Create the resources your helper expects to find, then call the helper.
# CLI: pytest tests/test_lookups.py::test_finds_tagged_vpc -v
# State implication: all resources live in moto's in-memory store and vanish at test exit.
import boto3
from moto import mock_aws
def find_vpc_by_tag(name: str, region: str = "us-east-1") -> str:
"""IaC helper: resolve a VPC id by its Name tag before building the stack."""
ec2 = boto3.client("ec2", region_name=region)
resp = ec2.describe_vpcs(Filters=[{"Name": "tag:Name", "Values": [name]}])
return resp["Vpcs"][0]["VpcId"]
@mock_aws
def test_finds_tagged_vpc() -> None:
ec2 = boto3.client("ec2", region_name="us-east-1")
created = ec2.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"]
ec2.create_tags(Resources=[created], Tags=[{"Key": "Name", "Value": "core"}])
assert find_vpc_by_tag("core") == created
3. Assert the calls your helper made
When the side effect matters more than the return value, wrap the real boto3 method with a spy to confirm the helper called it correctly.
# CLI: pytest tests/test_lookups.py::test_writes_parameter -v
from unittest.mock import patch
import boto3
from moto import mock_aws
def store_endpoint(name: str, value: str, region: str = "us-east-1") -> None:
# State implication: a real SSM PutParameter is a side effect; verify it is called once.
boto3.client("ssm", region_name=region).put_parameter(
Name=name, Value=value, Type="String", Overwrite=True
)
@mock_aws
def test_writes_parameter() -> None:
real_put = boto3.client("ssm", region_name="us-east-1").put_parameter
with patch("boto3.client") as mk:
mk.return_value.put_parameter.side_effect = real_put
store_endpoint("/app/endpoint", "https://api.internal")
mk.return_value.put_parameter.assert_called_once()
Verification
Run the suite and confirm it passes with no network access; the assertions prove both the return value and the call were correct.
# CLI: confirm the mocked suite is hermetic
pytest tests/ -q
# Provider note: unplug the network and it still passes — moto needs no connectivity.
Gotchas & Edge Cases
Missing region raises NoRegionError even under moto. moto patches the API, not boto3's configuration resolution. Always set AWS_DEFAULT_REGION (or pass region_name) or the client construction fails before the mock engages.
moto 5 removed the per-service decorators. Code written for moto 4 with @mock_ec2 or @mock_s3 breaks on upgrade. Use the single @mock_aws decorator (or with mock_aws():) for all services.
Real credentials can leak through if the fixture is missing. Without the dummy-credential fixture, boto3 may pick up a live profile and, for services moto does not fully emulate, hit real AWS. Keep the autouse env fixture in conftest.py so every test is isolated.
Frequently Asked Questions
Does moto support every AWS service? No. Core services (EC2, S3, IAM, SSM, DynamoDB, Lambda) are well covered, but coverage thins for newer or niche APIs. Check the moto implementation table for your service; for gaps, prefer LocalStack in an integration test.
Should I mock boto3 or use moto?
Use moto when you want realistic AWS behavior and state (create a VPC, then describe it). Use plain unittest.mock when you only need to assert that a specific call was made with specific arguments and do not care about realistic responses.
Can I share seeded moto state across tests?
Use a pytest fixture that enters mock_aws() and yields a seeded client. Each test still gets a fresh in-memory account because the context manager resets state on exit, which is what you want for isolation.
Related
- Testing Python Infrastructure Code — where moto fits in the broader IaC testing pyramid.
- Unit Testing Pulumi Programs with Mocks — the complementary technique for mocking the Pulumi runtime rather than the AWS SDK.
- Snapshot Testing CDKTF Stacks with pytest — assert synthesized output instead of SDK calls.