jguillaumesio
devopsaws

Testing Lambda and EventBridge locally without deploying

How to test AWS Lambda functions and EventBridge rules locally using SAM, LocalStack, and docker-compose. No cloud deployment needed.

Local Lambda + EventBridge testing pipeline

You change one line in a Lambda. You deploy to AWS. You wait 30 seconds. You test. It fails. You change one line. You deploy. You wait. This cycle is slow, expensive, and unnecessary.

Here’s how to test Lambda + EventBridge entirely on your laptop.

The Setup: docker-compose + LocalStack

The simplest approach uses LocalStack, a full AWS cloud stack running in Docker.

# docker-compose.yml
version: "3.8"
services:
  localstack:
    image: localstack/localstack:3.4
    ports:
      - "4566:4566"            # LocalStack Gateway
      - "4510-4559:4510-4559"  # external services port range
    environment:
      - SERVICES=lambda,events,logs,iam,sts
      - DEFAULT_REGION=eu-west-3
      - LAMBDA_EXECUTOR=docker-reuse
      - LAMBDA_REMOTE_DOCKER=false
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
      - "./localstack-init:/etc/localstack/init/ready.d"

Start it:

docker compose up -d

Creating a Lambda Locally

# Package your function
cd src/handler && zip -r ../../function.zip . && cd ..

# Create the Lambda
aws --endpoint-url=http://localhost:4566 lambda create-function \
  --function-name process-order \
  --runtime python3.12 \
  --handler handler.lambda_handler \
  --zip-file fileb://function.zip \
  --role arn:aws:iam::000000000000:role/lambda-role \
  --timeout 30 \
  --memory-size 256

# Invoke it
aws --endpoint-url=http://localhost:4566 lambda invoke \
  --function-name process-order \
  --payload '{"orderId": "12345", "action": "process"}' \
  --cli-binary-format raw-in-base64-out \
  output.json

cat output.json

Setting Up EventBridge Rules

# Create an event bus
aws --endpoint-url=http://localhost:4566 events create-event-bus \
  --name order-events

# Create a rule
aws --endpoint-url=http://localhost:4566 events put-rule \
  --name order-created-rule \
  --event-bus-name order-events \
  --event-pattern '{"source": ["order.service"], "detail-type": ["Order Created"]}'

# Add the Lambda as a target
aws --endpoint-url=http://localhost:4566 events put-targets \
  --rule order-created-rule \
  --event-bus-name order-events \
  --targets "Id"="1","Arn"="arn:aws:lambda:eu-west-3:000000000000:function:process-order"

Testing the Full Flow

# Send a test event
aws --endpoint-url=http://localhost:4566 events put-events \
  --entries '[
    {
      "Source": "order.service",
      "DetailType": "Order Created",
      "Detail": "{\"orderId\": \"ORD-001\", \"amount\": 99.99, \"customer\": \"[email protected]\"}",
      "EventBusName": "order-events"
    }
  ]'

# Check Lambda logs
aws --endpoint-url=http://localhost:4566 logs filter-log-events \
  --log-group-name /aws/lambda/process-order \
  --limit 10

Automated Testing with pytest

# tests/test_process_order.py
import json
import boto3
import pytest

@pytest.fixture
def lambda_client():
    return boto3.client(
        "lambda",
        endpoint_url="http://localhost:4566",
        region_name="eu-west-3",
        aws_access_key_id="test",
        aws_secret_access_key="test",
    )

@pytest.fixture
def events_client():
    return boto3.client(
        "events",
        endpoint_url="http://localhost:4566",
        region_name="eu-west-3",
        aws_access_key_id="test",
        aws_secret_access_key="test",
    )

def test_process_order_success(lambda_client):
    response = lambda_client.invoke(
        FunctionName="process-order",
        Payload=json.dumps({
            "orderId": "ORD-001",
            "action": "process"
        }),
    )

    payload = json.loads(response["Payload"].read())
    assert response["StatusCode"] == 200
    assert payload["status"] == "processed"

def test_eventbridge_triggers_lambda(events_client, lambda_client):
    # Send event
    events_client.put_events(Entries=[{
        "Source": "order.service",
        "DetailType": "Order Created",
        "Detail": json.dumps({"orderId": "ORD-002", "amount": 50.0}),
        "EventBusName": "order-events",
    }])

    # Wait for async processing
    import time; time.sleep(2)

    # Verify via CloudWatch Logs
    logs = boto3.client("logs", endpoint_url="http://localhost:4566",
                         region_name="eu-west-3",
                         aws_access_key_id="test",
                         aws_secret_access_key="test")

    events = logs.filter_log_events(
        logGroupName="/aws/lambda/process-order",
        limit=5,
    )

    log_messages = [e["message"] for e in events["events"]]
    assert any("ORD-002" in msg for msg in log_messages)

Run it:

docker compose up -d
./scripts/setup-localstack.sh   # create functions, rules, targets
pytest tests/ -v

Alternative: AWS SAM Local

If you’re already using SAM:

# Start local API and Lambda runtime
sam local start-lambda --docker-network host

# Invoke
sam local invoke process-order -e events/order-created.json

# Test EventBridge pattern matching
sam local generate-event events put-events \
  --source order.service \
  --detail-type "Order Created" \
  --detail '{"orderId": "12345"}'

SAM is lighter than LocalStack but only handles Lambda + API Gateway well. For EventBridge, LocalStack is better.

What LocalStack Doesn’t Simulate Well

Be aware of the gaps:

FeatureLocalStack SupportGotcha
Lambda invocation✅ GoodCold starts not simulated
EventBridge rules✅ GoodSome pattern matching edge cases differ
IAM permissions⚠️ PartialDoesn’t enforce all policies
Lambda concurrency❌ NoNo throttling simulation
VPC networking⚠️ PartialENI creation differs
CloudWatch metrics⚠️ BasicSome metric dimensions missing

For integration tests, LocalStack is fine. For performance or concurrency testing, you need the real cloud.

The Workflow

1. Write Lambda code
2. docker compose up -d          # start LocalStack
3. ./scripts/setup.sh            # deploy functions + rules locally
4. pytest tests/ -v              # run tests
5. docker compose down           # clean up

Total cycle time: under 5 seconds per test run. No cloud deployment. No waiting. No AWS bill for testing.

The Bottom Line

There’s no reason to deploy to AWS to test a Lambda. LocalStack gives you a functional local AWS environment in one docker compose up. The setup takes 10 minutes, and after that, your test-deploy cycle goes from minutes to seconds.

The only thing you can’t test locally is actual AWS service behavior at scale. For everything else (logic, event routing, error handling, integration between Lambda and EventBridge), local is faster, free, and more reliable.