mrkeyoor.com_
Thu 06 Aug 15:41 UTC
PyPITestingupdated 06 Aug 2026

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.

Verdict

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.

API stability4/5Version 5 replaced roughly a hundred per-service decorators with one mock_aws, which was a large but well-signposted migration, and the surface has been steady since; the config dict and its keys are still growing release by release.
Docs4/5docs.getmoto.org covers the decorator styles, server and proxy modes, multi-account setup, IAM access control, the recorder and every environment variable, plus a per-service coverage file; what is thin is guidance on where moto's behavior diverges from AWS, which you tend to discover from failing production code.
Maintenance5/5Pushed August 2026, 5.2.2 released in June 2026, 32 open issues (75 counting PRs), and a changelog that lists newly implemented API methods service by service in every release.
Ecosystem5/5Around 11 million downloads a week, funded through OpenCollective, and the assumed default in most Python projects that test against AWS, with a server mode that other language ecosystems point their SDKs at.

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
Skip it if

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 None

State 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"] == 1250

create_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 AWS

The 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

PackageRegistryPick it when
localstackPyPIYou want a container-based local AWS with higher fidelity and real Lambda execution, and you can afford the startup time and resource use.
responsesPyPIYou only need to stub a couple of HTTP calls and do not want a whole AWS implementation in your test dependencies.
vcrpyPyPIYou want to record real AWS responses once and replay them, accepting stale cassettes in exchange for exact fidelity to what AWS actually returned.