moto
moto lets your tests call AWS without an AWS account. Wrap a test in the @mock_aws decorator and every boto3 call inside it is intercepted before it reaches the network and answered by a Python reimplementation of that service, which keeps real state in memory: create a bucket, put an object, list the bucket, and the object is there. It covers a large slice of AWS (S3, DynamoDB, SQS, SNS, Lambda, IAM, EC2, Step Functions, Secrets Manager and dozens more) with per-service coverage tracked in the repository. Since version 5 there is a single decorator instead of the old per-service mock_s3 and mock_dynamodb family. It also runs as a standalone HTTP server so tests written in other languages, or code that shells out to the AWS CLI, can point at it with an endpoint_url.
The default way to test boto3 code, and version 5's single mock_aws decorator made it much easier to live with. Just remember it is a reimplementation, not AWS: keep at least a thin layer of tests against a real account for the paths where being wrong is expensive.
Use it if
- You have Python code using boto3 and want tests that run in milliseconds on a laptop with no AWS credentials, no network and no cleanup step
- You want to assert on the resulting AWS state, not just that a call was made: with moto you create the bucket and then read the object back, which a MagicMock patch cannot check
- You need to test error paths that are painful to trigger for real, such as a missing bucket, a throttled DynamoDB write or an SQS message that hits the visibility timeout
- Your stack is not only Python: moto_server or ThreadedMotoServer exposes the same mocks over HTTP so a Node service, a Java test suite or an aws CLI call can hit them through endpoint_url
- You need confidence that the code works against real AWS: moto is an independent reimplementation with partial coverage per service, documented in IMPLEMENTATION_COVERAGE.md, so a test can pass here and fail in production on a parameter moto ignores
- Your tests depend on IAM behavior: authorization is not enforced at all unless you opt in with INITIAL_NO_AUTH_ACTION_COUNT, so a test that should fail with AccessDenied happily succeeds
- You want a full local cloud rather than a test double: LocalStack runs the services as containers with more fidelity, real Lambda execution and a persistent UI, which is a better fit for manual and cross-team integration work
- Your CI cannot run Docker: Lambda and Batch execute code in containers by default, so those tests either need a Docker daemon or the use_docker False config, which then stops exercising your handler at all
- Dependency weight matters: moto[all] pulls docker, cfn-lint, antlr4-python3-runtime, graphql-core, openapi-spec-validator, joserfc and more, and the package carries pins against specific broken botocore and responses versions that can fight the rest of your lockfile
Setup reality
Install only the services you need, as in pip install 'moto[s3,dynamodb]'; moto[all] works but adds Docker, cfn-lint and a pile of parsers to every CI install. The mocking happens inside botocore, so ordering is what trips people up: any boto3 client or session created before the mock starts keeps talking to real endpoints, which is why module-level clients and cached sessions in application code are the classic cause of a test that unexpectedly reaches AWS. moto sets fake credentials while the mock is active, but the standard advice is still to put dummy AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SECURITY_TOKEN, AWS_SESSION_TOKEN and AWS_DEFAULT_REGION values into an autouse fixture so a badly ordered test fails instead of touching a real account. Every mocked resource lives in a virtual account (123456789012 by default) that starts empty, so tests must create their own buckets, tables and queues; state is reset between test methods even under a class-level decorator. Region matters: clients need region_name, and an unknown region raises unless MOTO_ALLOW_NONEXISTENT_REGION is set. For non-Python callers install moto[server] and run moto_server, or start ThreadedMotoServer in a pytest fixture with port=0 to get a free port.
Patterns
Mock every AWS call in one testdecorator-basics
import boto3
from moto import mock_aws
@mock_aws
def test_saves_to_s3():
s3 = boto3.client("s3", region_name="us-east-1")
s3.create_bucket(Bucket="mybucket")
save_report("mybucket", "report.csv", b"a,b,c")
body = s3.get_object(Bucket="mybucket", Key="report.csv")["Body"].read()
assert body == b"a,b,c"The virtual account starts empty, so the bucket has to be created inside the test even though production code assumes it exists. Create clients inside the decorated function: one built at import time was configured before the mock started and will talk to real AWS.
Use moto without a decoratorcontext-manager-and-manual
from moto import mock_aws
def test_with_context_manager():
with mock_aws():
s3 = boto3.client("s3", region_name="eu-west-1")
s3.create_bucket(
Bucket="b",
CreateBucketConfiguration={"LocationConstraint": "eu-west-1"},
)
class MyTest(unittest.TestCase):
def setUp(self):
self.mock = mock_aws()
self.mock.start()
boto3.client("s3", region_name="us-east-1").create_bucket(Bucket="b")
def tearDown(self):
self.mock.stop()start and stop let you set up shared state in setUp, which the decorator form cannot do. Note the region quirk that is real AWS behavior, not a moto one: every region except us-east-1 requires CreateBucketConfiguration.
Stop a misordered test from reaching real AWSpytest-credentials-fixture
# conftest.py
import os
import pytest
@pytest.fixture(autouse=True)
def aws_credentials(monkeypatch):
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
monkeypatch.setenv("AWS_SECURITY_TOKEN", "testing")
monkeypatch.setenv("AWS_SESSION_TOKEN", "testing")
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
monkeypatch.delenv("AWS_PROFILE", raising=False)moto supplies fake credentials while the mock is active, but this fixture is the seatbelt for the case where it is not: a client created outside the mock then fails with an auth error instead of quietly mutating a real account. Deleting AWS_PROFILE matters too, since a profile in ~/.aws/config can supply real keys.
Mock a whole test class and understand the resetclass-decorator
@mock_aws
class TestReports(unittest.TestCase):
def setUp(self):
boto3.client("s3", region_name="us-east-1").create_bucket(Bucket="mybucket")
def test_one(self):
s3 = boto3.client("s3", region_name="us-east-1")
s3.put_object(Bucket="mybucket", Key="a", Body=b"1")
def test_two(self):
s3 = boto3.client("s3", region_name="us-east-1")
# 'mybucket' exists again because setUp reran; the key from test_one does not
assert s3.list_objects_v2(Bucket="mybucket").get("Contents") is NoneState is destroyed before each test method, so tests cannot leak into each other and a tearDown that deletes resources is unnecessary. That also means you cannot build expensive shared state once for the class.
Create and query a DynamoDB tabledynamodb-table
@mock_aws
def test_orders_table():
ddb = boto3.resource("dynamodb", region_name="us-east-1")
ddb.create_table(
TableName="orders",
KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "pk", "AttributeType": "S"}],
BillingMode="PAY_PER_REQUEST",
)
table = ddb.Table("orders")
table.put_item(Item={"pk": "order#1", "total": 1250})
assert table.get_item(Key={"pk": "order#1"})["Item"]["total"] == 1250create_table returns immediately with the table ACTIVE, so there is no waiter to sit through, which is convenient and also hides the CREATING state your production code may need to handle. Numbers come back as Decimal exactly as they do from real DynamoDB.
Test Lambda plumbing on a machine with no Dockerlambda-without-docker
@mock_aws(config={"lambda": {"use_docker": False}, "batch": {"use_docker": False}})
def test_registers_function():
client = boto3.client("lambda", region_name="us-east-1")
client.create_function(
FunctionName="worker",
Runtime="python3.12",
Role="arn:aws:iam::123456789012:role/lambda-role",
Handler="main.handler",
Code={"ZipFile": zip_bytes},
)
assert client.get_function(FunctionName="worker")["Configuration"]["Handler"] == "main.handler"With use_docker True (the default) moto pulls a runtime image and actually executes your handler, which is the only way to assert on its output but needs a Docker daemon in CI. With it off, invoke returns a canned response, so you are testing the wiring and not the function body.
Let some calls through to the real worldpassthrough-real-calls
@mock_aws(config={
"core": {
"passthrough": {
"services": ["sts"],
"urls": ["https://internal-api.example.com/.*"],
},
"mock_credentials": False,
},
})
def test_hybrid():
...Useful when one dependency has to be real, for example an internal HTTP service that moto would otherwise swallow because it intercepts at the HTTP layer. Setting mock_credentials False makes moto use whatever credentials the environment supplies, so combine it with passthrough carefully: this is the configuration most likely to hit real AWS by accident.
Mock only the services you listlimit-mocked-services
@mock_aws(config={"core": {"service_whitelist": ["s3", "sqs"]}})
def test_only_s3_and_sqs():
boto3.client("s3", region_name="us-east-1").list_buckets()
# a call to any other service is not mocked and will try to reach AWSThe whitelist is a guard rail for large codebases: an unexpected call to a service you never intended to touch fails loudly instead of silently succeeding against a mock. Leave it as None (the default) to mock everything moto supports.
Run moto as an HTTP server for non-Python clientsthreaded-server
# pip install 'moto[server]'
import pytest
from moto.server import ThreadedMotoServer
@pytest.fixture(scope="module")
def moto_endpoint():
server = ThreadedMotoServer(port=0) # 0 picks a free port
server.start()
host, port = server.get_host_and_port()
yield f"http://{host}:{port}"
server.stop()
def test_via_endpoint(moto_endpoint):
s3 = boto3.client("s3", region_name="us-east-1", endpoint_url=moto_endpoint)
s3.create_bucket(Bucket="b")In server mode nothing is patched, so every client (including one in another process or another language) must be pointed at endpoint_url. State lives in the server and is not reset between tests, so either restart the server per module or clean up explicitly. Running the standalone binary is moto_server -p3000.
Test resources across two AWS accountsmulti-account
import os
from moto.core import DEFAULT_ACCOUNT_ID # 123456789012
@mock_aws
def test_two_accounts(monkeypatch):
s3 = boto3.client("s3", region_name="us-east-1")
s3.create_bucket(Bucket="bucket-default-account")
monkeypatch.setenv("MOTO_ACCOUNT_ID", "111111111111")
s3.create_bucket(Bucket="bucket-in-account-2")
assert [b["Name"] for b in s3.list_buckets()["Buckets"]] == ["bucket-in-account-2"]
monkeypatch.delenv("MOTO_ACCOUNT_ID")
assert [b["Name"] for b in s3.list_buckets()["Buckets"]] == ["bucket-default-account"]The environment variable switches which virtual account subsequent requests land in, and each account keeps its own isolated state; the same client object is reused. In server mode you can send an account ID as a request header instead, but only when the environment variable is unset.
Actually enforce IAM policies in a testiam-enforcement
from moto.core import set_initial_no_auth_action_count
@set_initial_no_auth_action_count(4)
@mock_aws
def test_denied_without_permission():
iam = boto3.client("iam", region_name="us-east-1")
iam.create_user(UserName="limited") # call 1
key = iam.create_access_key(UserName="limited")["AccessKey"] # call 2
# ... exactly 4 setup calls before enforcement begins
ec2 = boto3.client(
"ec2",
region_name="us-east-1",
aws_access_key_id=key["AccessKeyId"],
aws_secret_access_key=key["SecretAccessKey"],
)
with pytest.raises(ClientError):
ec2.describe_instances()Without this decorator or the INITIAL_NO_AUTH_ACTION_COUNT environment variable, moto never authorizes anything, so no test of yours can prove a policy denies access. The count is the number of calls made before enforcement kicks in, and getting it wrong by one turns your setup calls into the thing being denied.
Really execute a Step Functions state machinestep-functions-execution
@mock_aws(config={"stepfunctions": {"execute_state_machine": True}})
def test_state_machine_runs():
sfn = boto3.client("stepfunctions", region_name="us-east-1")
machine = sfn.create_state_machine(
name="pipeline",
definition=json.dumps(definition),
roleArn="arn:aws:iam::123456789012:role/sfn",
)
run = sfn.start_execution(stateMachineArn=machine["stateMachineArn"])
assert sfn.describe_execution(executionArn=run["executionArn"])["status"] in (
"RUNNING", "SUCCEEDED",
)By default moto records the execution and reports success without interpreting the state machine, so a broken definition still looks fine. Turning execute_state_machine on runs the states for real and needs the extra parser dependencies from moto[stepfunctions].
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| localstack | PyPI | You want a container-based local AWS with higher fidelity and real Lambda execution, and you can afford the startup time and resource use. |
| responses | PyPI | You only need to stub a couple of HTTP calls and do not want a whole AWS implementation in your test dependencies. |
| vcrpy | PyPI | You want to record real AWS responses once and replay them, accepting stale cassettes in exchange for exact fidelity to what AWS actually returned. |