moto for Python: Mocking boto3 in Tests
moto is a Python library that mocks AWS services in-process, so boto3 calls in your tests never leave your machine.
Search across all documentation pages
moto is a Python library that mocks AWS services in-process, so boto3 calls in your tests never leave your machine.
It works by intercepting the HTTP calls botocore makes and returning simulated AWS responses, backed by an in-memory model of each service's state. Your application code doesn't know the difference - it still calls boto3.client("s3") and gets back real-shaped responses.
The current moto API is one decorator,
@mock_aws, that mocks every AWS service at once. Older per-service decorators like@mock_s3still exist in some codebases but are deprecated in moto v5 in favor of the unified form.
# --- Python (boto3) ---
import boto3
from moto import mock_aws
@mock_aws
def test_create_and_list_bucket():
s3 = boto3.client("s3", region_name="us-east-1")
s3.create_bucket(Bucket="my-test-bucket")
names = [b["Name"] for b in s3.list_buckets()["Buckets"]]
assert "my-test-bucket" in names// --- TypeScript (AWS SDK v3) ---
// The equivalent test pattern in the TypeScript ecosystem uses
// aws-sdk-client-mock, not moto (moto is Python-only).
import { S3Client, CreateBucketCommand, ListBucketsCommand } from "@aws-sdk/client-s3";
import { mockClient } from "aws-sdk-client-mock";
const s3Mock = mockClient(S3Client);
test("create and list bucket", async () => {
s3Mock.on(CreateBucketCommand).resolves({});
s3Mock.on(ListBucketsCommand).resolves({ Buckets: [{ Name: "my-test-bucket" }] });
const s3 = new S3Client({});
await s3.send(new CreateBucketCommand({ Bucket: "my-test-bucket" }));
const res = await s3.send(new ListBucketsCommand({}));
expect(res.Buckets?.map((b) => b.Name)).toContain("my-test-bucket");
});When to reach for this:
ClientError shapes real AWS would.A more complete test that seeds state, exercises a function, and checks both the return value and side effects.
# --- Python (boto3) ---
import boto3
import pytest
from moto import mock_aws
from botocore.exceptions import ClientError
def upsert_user_count(table_name: str, user_id: str) -> int:
ddb = boto3.resource("dynamodb")
table = ddb.Table(table_name)
resp = table.update_item(
Key={"id": user_id},
UpdateExpression="ADD visits :one",
ExpressionAttributeValues={":one": 1},
ReturnValues="UPDATED_NEW",
)
return int(resp["Attributes"]["visits"])
@pytest.fixture
def ddb_table():
with mock_aws():
ddb = boto3.resource("dynamodb", region_name="us-east-1")
ddb.create_table(
TableName="Visits",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
BillingMode="PAY_PER_REQUEST",
)
yield "Visits"
def test_upsert_user_count_increments(ddb_table):
assert upsert_user_count(ddb_table, "user-7") == 1
assert upsert_user_count(ddb_table, "user-7") == 2
def test_upsert_user_count_missing_table_raises():
# No fixture here - no table exists in this mocked account.
with mock_aws():
with pytest.raises(ClientError) as exc:
upsert_user_count("NoSuchTable", "user-7")
assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException"// --- TypeScript (AWS SDK v3) ---
// aws-sdk-client-mock equivalent: stub the two DynamoDB commands
// the function under test actually calls.
import {
DynamoDBClient,
UpdateItemCommand,
ResourceNotFoundException,
} from "@aws-sdk/client-dynamodb";
import { mockClient } from "aws-sdk-client-mock";
async function upsertUserCount(ddb: DynamoDBClient, tableName: string, userId: string): Promise<number> {
const res = await ddb.send(new UpdateItemCommand({
TableName: tableName,
Key: { id: { S: userId } },
UpdateExpression: "ADD visits :one",
ExpressionAttributeValues: { ":one": { N: "1" } },
ReturnValues: "UPDATED_NEW",
}));
return Number(res.Attributes?.visits.N);
}
const ddbMock = mockClient(DynamoDBClient);
test("increments on repeated calls", async () => {
ddbMock.on(UpdateItemCommand)
.resolvesOnce({ Attributes: { visits: { N: "1" } } })
.resolvesOnce({ Attributes: { visits: { N: "2" } } });
const ddb = new DynamoDBClient({});
expect(await upsertUserCount(ddb, "Visits", "user-7")).toBe(1);
expect(await upsertUserCount(ddb, "Visits", "user-7")).toBe(2);
});
test("missing table rejects", async () => {
ddbMock.on(UpdateItemCommand).rejects(
new ResourceNotFoundException({ message: "not found", $metadata: {} }),
);
await expect(upsertUserCount(new DynamoDBClient({}), "NoSuchTable", "user-7"))
.rejects.toBeInstanceOf(ResourceNotFoundException);
});What this demonstrates:
mock_aws() context manager works as a fixture, letting you create real tables/buckets once and reuse them across tests.ClientError/ResourceNotFoundException shape for a missing table, so your error-handling code is tested against realistic errors.@mock_aws and Not the Old Per-Service Decorators@mock_s3, @mock_dynamodb, @mock_sqs, and so on) into one @mock_aws decorator.@mock_aws exclusively.@mock_aws mocks all services at once, which matters when your code touches more than one service in the same test.with mock_aws():), or a pytest fixture, whichever fits your test structure.@mock_aws inside another mocked context is unnecessary - one mock covers every service call inside its scope.@mock_s3 still works in some moto versions but is being phased out. Fix: use @mock_aws in all new code.boto3.client(...) built outside the @mock_aws scope may bind to real endpoints. Fix: always construct clients inside the decorated function or context manager.region_name explicitly rather than relying on environment defaults in CI.Code in err.response["Error"]), never the message string.AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env vars in your test environment; moto never validates or uses them.| Alternative | Use When | Don't Use When |
|---|---|---|
@mock_aws (moto) | Python unit tests, in-process, no Docker | You need cross-service integration behavior a simulation can't model |
| LocalStack | Integration tests needing a real server and closer-to-real behavior | A simple in-process mock would already catch the bug |
| Contract test vs real AWS | Verifying real IAM/quota edges before release | Everyday unit testing - too slow and costly to run often |
| Hand-rolled fake client | A narrow, very specific interface you fully control | You'd just be reimplementing what moto already does |
It still works in current moto releases for backward compatibility, but it's deprecated. New code should use the unified @mock_aws decorator, which covers every service in one mock scope.
No. It patches botocore's transport layer so every intercepted call is served in-process from moto's simulated backend, with no network access at all.
Yes, and it's the recommended pattern. Make real boto3 calls (create_bucket, put_item, etc.) inside the mocked scope to set up state, then call your code under test against it.
It models the common ones closely - not-found errors, validation errors, conflict errors - well enough to test your error-handling logic. Very obscure or newly launched behaviors may have thinner coverage.
No. moto runs entirely in the Python test process; it needs no container and no separate server, which is what makes it fast enough for a large unit test suite.
moto is an in-process Python library mocking service HTTP calls directly; LocalStack is a real server (a Docker container) your SDK client points at over an endpoint. LocalStack is heavier but gives closer-to-real, cross-service behavior.
boto3 wants something present in the credential chain syntactically, so set dummy environment variables. moto never validates or uses the actual values.
Stack versions: This page was written for boto3 1.43.x (Python 3.10+) and the AWS SDK for JavaScript v3 (Node.js 18+).
Reviewed by Chris St. John·Last updated Jul 23, 2026