Skip to content

Fix: Moto Not Working — Mock Decorator, Real AWS Calls Leaking, and v4 to v5 Migration

FixDevs ·

Quick Answer

How to fix Moto errors — mock not activating, real AWS credentials used in tests, ImportError mock_s3 removed in v5, fixtures with multiple services, NoCredentialsError despite mock, and standalone server mode.

The Error

You decorate a test with @mock_s3 and Moto says it doesn’t exist:

from moto import mock_s3   # ImportError in moto 5.x

@mock_s3
def test_upload():
    ...

Or @mock_aws activates but the test still hits real AWS:

from moto import mock_aws
import boto3

@mock_aws
def test_s3():
    s3 = boto3.client("s3", region_name="us-east-1")
    s3.list_buckets()
    # Returns your REAL buckets, not mocked ones

Or tests pass individually but fail when run together:

$ pytest test_one.py        # Passes
$ pytest test_two.py        # Passes
$ pytest test_one.py test_two.py   # One of them fails — state leak

Or you get NoCredentialsError inside a mocked test:

botocore.exceptions.NoCredentialsError: Unable to locate credentials

Or DynamoDB queries silently return empty:

table.put_item(Item={"id": "1", "name": "Alice"})
response = table.get_item(Key={"id": "1"})
print(response.get("Item"))   # None — but you just put it!

Moto is the canonical AWS mocking library for Python tests — it provides drop-in replacements for boto3 against in-memory implementations of S3, DynamoDB, SQS, Lambda, IAM, and dozens more services. Moto 5.x (March 2024) consolidated all per-service mocks into a single mock_aws decorator, breaking countless tutorials. This guide covers each common failure.

Why This Happens

Moto patches botocore at the HTTP layer — every AWS API call from boto3 gets intercepted and routed to Moto’s in-memory implementation instead of real AWS. The mock only activates within the decorated context (function, fixture, or class); outside it, boto3 makes real API calls.

The v4 → v5 migration replaced per-service decorators (@mock_s3, @mock_dynamodb) with a unified @mock_aws. Code written for v4 fails on import in v5. The new decorator covers all services automatically.

Fix 1: v4 to v5 Migration

# OLD — moto v4 (broken in v5)
from moto import mock_s3, mock_dynamodb, mock_sqs

@mock_s3
@mock_dynamodb
def test_pipeline():
    ...

# NEW — moto v5
from moto import mock_aws

@mock_aws
def test_pipeline():
    ...

mock_aws covers every AWS service Moto supports. No need to list them individually.

Install the right version:

# Latest moto v5+
pip install moto

# Or with extras for specific services
pip install "moto[s3,dynamodb,sqs]"
pip install "moto[all]"        # All services
pip install "moto[server]"      # Standalone HTTP server mode

Common Mistake: Following a tutorial that uses @mock_s3 and importing fails with ImportError: cannot import name 'mock_s3'. The fix is always from moto import mock_aws — the per-service decorators are gone. Tutorials from before March 2024 use the old API.

Fix 2: Real AWS Calls Leaking Despite Mock

from moto import mock_aws
import boto3

@mock_aws
def test_s3():
    s3 = boto3.client("s3")
    s3.list_buckets()
    # Hits real AWS if AWS_PROFILE or AWS_ACCESS_KEY_ID is set

Moto patches botocore for the duration of the decorator. But boto3.client() reads credentials at creation time — if those credentials are real, the client may bypass the mock in some setups.

Set fake credentials before the test:

import os
import pytest
from moto import mock_aws
import boto3

@pytest.fixture(autouse=True)
def aws_credentials():
    """Mocked AWS Credentials for moto."""
    os.environ["AWS_ACCESS_KEY_ID"] = "testing"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
    os.environ["AWS_SECURITY_TOKEN"] = "testing"
    os.environ["AWS_SESSION_TOKEN"] = "testing"
    os.environ["AWS_DEFAULT_REGION"] = "us-east-1"

@mock_aws
def test_s3():
    s3 = boto3.client("s3")
    s3.create_bucket(Bucket="my-bucket")
    assert "my-bucket" in [b["Name"] for b in s3.list_buckets()["Buckets"]]

Pro Tip: Always include an autouse=True fixture that sets fake AWS credentials. It guarantees no test accidentally uses real credentials — even one bare boto3.client() call without @mock_aws would otherwise hit production. Set this in conftest.py so it applies to every test in the project.

Verify mocking with a smoke test:

@mock_aws
def test_smoke():
    s3 = boto3.client("s3")
    response = s3.list_buckets()
    # Fresh moto state always has 0 buckets
    assert len(response["Buckets"]) == 0, "Moto isn't active — got real buckets"

Fix 3: Decorator vs Context Manager vs Fixture

Decorator (simplest for single-test mocks):

from moto import mock_aws

@mock_aws
def test_s3():
    ...

Context manager (for partial-test mocking):

from moto import mock_aws

def test_partial():
    # Real code here (if any)
    with mock_aws():
        # Mocked AWS calls
        s3 = boto3.client("s3")
        s3.create_bucket(Bucket="test")
    # Mocked context exits — boto3 reverts to real AWS

Class decorator (mocks all methods):

@mock_aws
class TestS3:
    def test_one(self):
        ...
    def test_two(self):
        ...

pytest fixture (reusable across tests):

import pytest
from moto import mock_aws
import boto3

@pytest.fixture
def s3_client():
    with mock_aws():
        client = boto3.client("s3", region_name="us-east-1")
        client.create_bucket(Bucket="test-bucket")
        yield client
        # Mock exits here, cleaning up state

def test_upload(s3_client):
    s3_client.put_object(Bucket="test-bucket", Key="file.txt", Body=b"hello")
    response = s3_client.get_object(Bucket="test-bucket", Key="file.txt")
    assert response["Body"].read() == b"hello"

For pytest fixture patterns that pair with Moto, see pytest fixture not found.

Fix 4: State Leakage Between Tests

$ pytest test_a.py test_b.py
# test_a passes, test_b fails because S3 has leftover state from test_a

By default, Moto state lives for the entire process — buckets created in one test persist into the next.

Use fresh mock per test:

import pytest
from moto import mock_aws
import boto3

@pytest.fixture
def s3():
    with mock_aws():
        yield boto3.client("s3", region_name="us-east-1")
    # Mock exits — all moto state cleared

def test_one(s3):
    s3.create_bucket(Bucket="test")
    assert len(s3.list_buckets()["Buckets"]) == 1

def test_two(s3):
    # Fresh state — no buckets from test_one
    assert len(s3.list_buckets()["Buckets"]) == 0

Or reset explicitly between tests:

from moto.core import reset_model_data

@pytest.fixture(autouse=True)
def reset_moto():
    yield
    reset_model_data()   # Wipe all moto state after each test

Common Mistake: Using @mock_aws at the module level — the mock activates once when the module loads and persists across every test in the file. State leaks freely. Always scope the mock per test or per fixture (function-scoped), not module-scoped.

Fix 5: Multi-Region and Multi-Service Setups

from moto import mock_aws
import boto3

@mock_aws
def test_multi_region():
    # Different regions are independent in Moto
    us_s3 = boto3.client("s3", region_name="us-east-1")
    eu_s3 = boto3.client("s3", region_name="eu-west-1")

    us_s3.create_bucket(Bucket="us-bucket")
    eu_s3.create_bucket(
        Bucket="eu-bucket",
        CreateBucketConfiguration={"LocationConstraint": "eu-west-1"},
    )

    assert "us-bucket" in [b["Name"] for b in us_s3.list_buckets()["Buckets"]]
    # Note: list_buckets returns global, both regions

Multi-service workflow:

@mock_aws
def test_sqs_to_lambda_to_s3():
    sqs = boto3.client("sqs", region_name="us-east-1")
    s3 = boto3.client("s3", region_name="us-east-1")
    lambda_client = boto3.client("lambda", region_name="us-east-1")
    iam = boto3.client("iam", region_name="us-east-1")

    # Create role
    role = iam.create_role(
        RoleName="lambda-exec",
        AssumeRolePolicyDocument=json.dumps({...}),
    )

    # Create queue
    queue = sqs.create_queue(QueueName="my-queue")

    # Create bucket
    s3.create_bucket(Bucket="my-bucket")

    # Create lambda (you control function code in tests)
    lambda_client.create_function(
        FunctionName="my-fn",
        Runtime="python3.12",
        Role=role["Role"]["Arn"],
        Handler="handler.lambda_handler",
        Code={"ZipFile": minimal_zip_with_handler()},
    )

All services share the same Moto state for the duration of the mock — they can reference each other (Lambda triggering on S3 events, etc., though some advanced integrations don’t fully emulate the real AWS behavior).

Fix 6: Standalone Server Mode

For non-Python clients or end-to-end tests where you want a real HTTP endpoint:

pip install "moto[server]"
moto_server -p 5000
import boto3

s3 = boto3.client(
    "s3",
    endpoint_url="http://localhost:5000",
    region_name="us-east-1",
    aws_access_key_id="testing",
    aws_secret_access_key="testing",
)

s3.create_bucket(Bucket="test")

Docker:

docker run -p 5000:5000 motoserver/moto:latest
# docker-compose.yml for integration tests
services:
  moto:
    image: motoserver/moto:latest
    ports:
      - "5000:5000"

  app:
    build: .
    environment:
      AWS_ENDPOINT_URL: http://moto:5000
      AWS_ACCESS_KEY_ID: testing
      AWS_SECRET_ACCESS_KEY: testing
    depends_on:
      - moto

AWS_ENDPOINT_URL (boto3 1.34+) — set once, all clients use it:

export AWS_ENDPOINT_URL=http://localhost:5000
python my_app.py   # All boto3 calls go to moto_server

Standalone server mode is essential for testing applications that aren’t pure Python — Go services, Lambda functions running in containers, multi-process workers.

Fix 7: DynamoDB Specific Patterns

DynamoDB is one of the most-used Moto services. Common patterns:

import boto3
from boto3.dynamodb.conditions import Key, Attr
from moto import mock_aws

@mock_aws
def test_dynamodb():
    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")

    # Create table
    table = dynamodb.create_table(
        TableName="Users",
        KeySchema=[
            {"AttributeName": "user_id", "KeyType": "HASH"},
        ],
        AttributeDefinitions=[
            {"AttributeName": "user_id", "AttributeType": "S"},
        ],
        BillingMode="PAY_PER_REQUEST",
    )

    # Wait for table to become active (Moto creates synchronously, but real DDB doesn't)
    table.wait_until_exists()

    # Put items
    table.put_item(Item={"user_id": "1", "name": "Alice", "age": 30})
    table.put_item(Item={"user_id": "2", "name": "Bob", "age": 25})

    # Get item
    response = table.get_item(Key={"user_id": "1"})
    assert response["Item"]["name"] == "Alice"

    # Query (requires partition key)
    response = table.query(
        KeyConditionExpression=Key("user_id").eq("1"),
    )

    # Scan (full table)
    response = table.scan(FilterExpression=Attr("age").gt(20))

Common Mistake: Forgetting to call wait_until_exists() after creating a table. In Moto, table creation is synchronous — but real AWS DynamoDB takes time to provision. Your tests pass in Moto and fail in production because production tests start querying before the table is ready. Always include wait_until_exists() so tests catch the issue early.

Global Secondary Index (GSI):

table = dynamodb.create_table(
    TableName="Users",
    KeySchema=[
        {"AttributeName": "user_id", "KeyType": "HASH"},
    ],
    AttributeDefinitions=[
        {"AttributeName": "user_id", "AttributeType": "S"},
        {"AttributeName": "email", "AttributeType": "S"},
    ],
    GlobalSecondaryIndexes=[
        {
            "IndexName": "email-index",
            "KeySchema": [{"AttributeName": "email", "KeyType": "HASH"}],
            "Projection": {"ProjectionType": "ALL"},
        },
    ],
    BillingMode="PAY_PER_REQUEST",
)

table.put_item(Item={"user_id": "1", "email": "[email protected]"})

# Query by email via GSI
response = table.query(
    IndexName="email-index",
    KeyConditionExpression=Key("email").eq("[email protected]"),
)

Fix 8: Lambda and Async Patterns

import io
import json
import zipfile
from moto import mock_aws

def create_lambda_zip():
    """Create a minimal zip for Moto Lambda."""
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, "w") as zf:
        zf.writestr(
            "handler.py",
            "def lambda_handler(event, context):\n"
            "    return {'statusCode': 200, 'body': 'hello'}",
        )
    buf.seek(0)
    return buf.read()

@mock_aws
def test_lambda():
    iam = boto3.client("iam", region_name="us-east-1")
    lambda_client = boto3.client("lambda", region_name="us-east-1")

    role = iam.create_role(
        RoleName="lambda-role",
        AssumeRolePolicyDocument=json.dumps({
            "Version": "2012-10-17",
            "Statement": [{
                "Effect": "Allow",
                "Principal": {"Service": "lambda.amazonaws.com"},
                "Action": "sts:AssumeRole",
            }],
        }),
    )

    lambda_client.create_function(
        FunctionName="my-fn",
        Runtime="python3.12",
        Role=role["Role"]["Arn"],
        Handler="handler.lambda_handler",
        Code={"ZipFile": create_lambda_zip()},
    )

    # Invoke
    response = lambda_client.invoke(
        FunctionName="my-fn",
        Payload=json.dumps({"key": "value"}),
    )

    payload = json.loads(response["Payload"].read())
    assert payload["statusCode"] == 200

Important caveat — Moto Lambda executes the function code in a separate process by default. For most CI environments this works, but if your Lambda has heavy dependencies, mock them or use MOTO_LAMBDA_STUB_ECR=true to skip image-based functions.

Still Not Working?

Moto Limits and Real AWS Differences

Moto is an emulation — not 100% behavioral parity. Common gaps:

  • IAM policy evaluation is approximate, not full SCP/permission boundary logic
  • S3 event notifications to Lambda work but with timing differences
  • DynamoDB streams work but pagination behavior may differ
  • Cognito user pool sign-in flows are simplified
  • EventBridge custom buses have limited integration testing

For features where Moto isn’t fully accurate, supplement with integration tests against real AWS in a separate test stage.

Speeding Up Test Suites

# Avoid moto setup overhead for tests that don't touch AWS
@pytest.fixture
def aws():
    with mock_aws():
        yield

Only fixtures that explicitly use aws get the mock — non-AWS tests skip Moto entirely.

Combining Moto with localstack

LocalStack is the “heavier” alternative — runs as a Docker container, supports more services and edge cases. For pure unit tests, Moto is faster. For end-to-end tests of complex workflows, LocalStack is more accurate.

A common split: Moto for fast unit tests, LocalStack (or real AWS staging) for integration tests.

Combining with pytest

import pytest
from moto import mock_aws
import boto3

@pytest.fixture(scope="function")
def aws_credentials():
    import os
    os.environ["AWS_ACCESS_KEY_ID"] = "testing"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
    os.environ["AWS_DEFAULT_REGION"] = "us-east-1"

@pytest.fixture
def s3(aws_credentials):
    with mock_aws():
        yield boto3.client("s3")

@pytest.fixture
def dynamodb(aws_credentials):
    with mock_aws():
        yield boto3.resource("dynamodb")

# Use them independently
def test_s3(s3):
    s3.create_bucket(Bucket="test")

def test_dynamodb(dynamodb):
    table = dynamodb.create_table(...)

For pytest async fixture patterns when testing async AWS workflows, see pytest fixture not found.

Testing with Async boto3 (aioboto3)

Moto’s mock_aws intercepts botocore — aioboto3 uses the same botocore under the hood, so Moto works:

import pytest
import aioboto3
from moto import mock_aws

@pytest.mark.asyncio
async def test_async_s3():
    with mock_aws():
        session = aioboto3.Session()
        async with session.client("s3", region_name="us-east-1") as s3:
            await s3.create_bucket(Bucket="test")
            response = await s3.list_buckets()
            assert len(response["Buckets"]) == 1

For asyncio event loop issues that affect aioboto3 + Moto, see Python asyncio not running.

Snapshot Testing with Moto

For complex setups, snapshot the Moto state for test assertions:

import json
from moto import mock_aws

@mock_aws
def test_full_workflow():
    setup_resources()
    run_workflow()

    # Snapshot the resulting state
    s3 = boto3.client("s3")
    buckets = sorted([b["Name"] for b in s3.list_buckets()["Buckets"]])
    assert buckets == ["expected-bucket-1", "expected-bucket-2"]

For LocalStack-based alternatives when Moto’s behavior isn’t sufficient, integration tests against real AWS staging are the next step. For broader AWS testing patterns, see AWS Lambda timeout for runtime-side considerations.

Pre-Configured Mock Resources via Fixtures

For test suites that always need certain resources, package the setup into reusable fixtures:

import pytest
import boto3
from moto import mock_aws

@pytest.fixture
def aws_environment():
    """Standard test AWS environment with pre-created resources."""
    with mock_aws():
        # S3 buckets
        s3 = boto3.client("s3", region_name="us-east-1")
        s3.create_bucket(Bucket="app-data")
        s3.create_bucket(Bucket="app-logs")

        # DynamoDB tables
        dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
        users_table = dynamodb.create_table(
            TableName="users",
            KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
            AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
            BillingMode="PAY_PER_REQUEST",
        )
        users_table.wait_until_exists()

        # SQS queues
        sqs = boto3.client("sqs", region_name="us-east-1")
        sqs.create_queue(QueueName="work-queue")
        sqs.create_queue(QueueName="dlq")

        yield {
            "s3": s3,
            "dynamodb": dynamodb,
            "sqs": sqs,
        }

def test_my_app(aws_environment):
    s3 = aws_environment["s3"]
    # Test against the pre-configured environment

This reduces per-test boilerplate dramatically — every test starts with a known-good AWS environment.

For pre-commit hooks that validate boto3 + Moto usage patterns, see pre-commit not working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles