Exam Guide: Developer - Associate
ποΈ Domain 3: Deployment
π Task 2: Test Applications In Development Environments
Testing on AWS isn't just about unit tests. You need to test locally with SAM CLI, write integration tests against deployed stages, mock AWS services and external APIs, validate event-driven flows, and use API Gateway stages for environment isolation.
πConcepts
The Testing Pyramid
| Level | Speed | Scope | AWS Services | Tools |
|---|---|---|---|---|
| Unit Tests | Fast (ms) | Single function, no external calls | Mocked | pytest, moto, unittest.mock |
| Integration Tests | Medium (seconds) | Multiple services working together | Real (deployed) | pytest + requests, deployed API Gateway stage |
| End-to-end Tests | Slow (secondsβminutes) | Full user workflow | Real (deployed) | Selenium, Postman, curl against live API |
π‘ Unit tests use mocks (moto for AWS, unittest.mock for external APIs). Integration tests run against real deployed resources.
SAM Local Testing Capabilities
Command |
What It Does | When to Use |
|---|---|---|
sam local invoke |
Runs a single Lambda function locally in a Docker container | Testing individual function logic with a specific event |
sam local start-api |
Starts a local API Gateway emulator | Testing full API request/response flow locally |
sam local start-lambda |
Starts a local Lambda endpoint for SDK calls | Testing code that invokes Lambda via the SDK |
sam local generate-event |
Generates sample event payloads for AWS services | Creating test events for S3, SQS, API Gateway, etc. |
β οΈ Key Details:
- SAM local uses Docker to simulate the Lambda runtime environment
- It reads your
template.yamlto understand function configuration - Environment variables can be overridden with
--env-vars env.json - It does not mock AWS services, therefore if your function calls DynamoDB, it calls real DynamoDB (or you need moto)
π‘
sam local invokeruns a function once and exits.sam local start-apikeeps running and accepts HTTP requests. Both use Docker containers that match the Lambda runtime. Know that SAM local does NOT mock AWS services. Your function still needs real credentials or mocked services.
Mocking Strategies
| What to Mock | Tool | How It Works |
|---|---|---|
| AWS Services (DynamoDB, S3, SQS, etc.) | moto (@mock_aws) |
Intercepts boto3 calls and returns realistic responses |
| External HTTP APIs | unittest.mock (@patch) |
Replaces the HTTP client with a mock that returns controlled responses |
| Environment Variables |
os.environ or monkeypatch
|
Set test values for TABLE_NAME, STAGE, etc. |
| Lambda Context | Custom object | Create a simple object with function_name, memory_limit_in_mb, etc. |
API Gateway Stages for Environment Isolation
| Stage | URL Pattern |
Use Case |
|---|---|---|
| dev | https://{api-id}.execute-api.{region}.amazonaws.com/dev |
Development and local integration testing |
| staging | https://{api-id}.execute-api.{region}.amazonaws.com/staging |
QA and pre-production testing |
| prod | https://{api-id}.execute-api.{region}.amazonaws.com/prod |
Production traffic |
Each stage can have its own:
- Stage Variables: different table names, feature flags, log levels per stage
- Throttling Settings: different rate limits per stage
- Logging Configuration: verbose logging in dev, errors only in prod
π‘ Stage variables are accessible in Lambda via
event['stageVariables']. They let you point different stages at different backends (different DynamoDB tables, different Lambda aliases) without changing code.
Event-Driven Testing Approaches
| Approach | What It Tests | How |
|---|---|---|
| SAM Local With Generated Events | Function logic with realistic payloads |
sam local generate-event β sam local invoke
|
| Console Test Events | Function logic in the deployed environment | Create test events in the Lambda console |
| EventBridge Test Events | Rule matching and target invocation | Send test events via the EventBridge console |
| SQS Test Messages | Queue-triggered function processing | Send messages via the SQS console |
| CloudWatch Logs | Verify function executed correctly | Check logs after sending test events |
ποΈ Build A Testing Toolkit
Build a Testing Toolkit that demonstrates testing approaches:
- Local Lambda testing with SAM CLI (
invokeandstart-api) - Unit tests using moto to mock DynamoDB and S3
- Integration tests running against a deployed API Gateway stage
- EventBridge rule testing with test events in the console
- Sample event generation with
sam local generate-event
Prerequisites
β οΈ Before You Start Read This
- π‘ SAM local does NOT mock AWS.
sam local invokeandsam local start-apirun your code in a container but call real AWS services with your real credentials. So we create the DynamoDB table first.- π‘ API Gateway routes come from the template, not your code. A path only exists if it's declared in the template's
Events.GET /will always say "Missing Authentication Token" because there's no root route.- π‘ Region must be consistent across your credentials, the table, and
env.json. We pinus-east-1everywhere so change it only if you use a different region, but change it everywhere.- π‘ Windows files need UTF-8 without BOM. A BOM causes
Unable to unmarshal input: Expecting value: line 1 column 1. Create JSON files in VS Code and check the encoding in the bottom-right status bar.- π‘ After editing
template.yaml, alwayssam buildthen restartstart-api. The local server serves the built template in.aws-sam/build/, not your edited source.
Step 00: Set your region once for this session so every command agrees:
$env:AWS_REGION = "us-east-1"
aws configure set region us-east-1
β οΈ If you already use a different region, substitute it consistently in every command and file below.
Step 01: Create the DynamoDB Table First
β οΈ Because SAM local calls real DynamoDB, the table must exist before you invoke anything.
aws dynamodb create-table `
--table-name dev-orders `
--attribute-definitions AttributeName=PK,AttributeType=S AttributeName=SK,AttributeType=S `
--key-schema AttributeName=PK,KeyType=HASH AttributeName=SK,KeyType=RANGE `
--billing-mode PAY_PER_REQUEST `
--region us-east-1
π‘ The backtick
`is PowerShell's line-continuation character. On macOS/Linux/CloudShell, use\instead.β οΈ Wait until the table is
ACTIVE:
aws dynamodb describe-table --table-name dev-orders --region us-east-1 --query "Table.TableStatus"
β οΈ Repeat until it prints
"ACTIVE"(usually a few seconds).π‘ The table's key schema mirrors single-table design.
PK(partition key) +SK(sort key) let one table hold many item types. This is the DynamoDB pattern the exam expects.
Step 02: Create the SAM Project
sam init --runtime python3.13 --name testing-toolkit --app-template hello-world
cd testing-toolkit
π‘This scaffolds a project with
hello_world/app.py,template.yaml, andtests/.
Step 03: Replace the Function Code
Open hello_world/app.py and replace its entire contents with this:
import json
import os
from datetime import datetime
import boto3
# π‘ Initialize the client OUTSIDE the handler so it's reused across warm
# invocations. Creating it inside the handler would rebuild it every call.
dynamodb = boto3.resource("dynamodb")
TABLE_NAME = os.environ.get("TABLE_NAME", "dev-orders")
def lambda_handler(event, context):
"""Order API: create an order (POST) or fetch one (GET)."""
table = dynamodb.Table(TABLE_NAME)
method = event.get("httpMethod", "GET")
path = event.get("path", "/")
try:
# --- Create an order ---
if method == "POST" and path == "/orders":
body = json.loads(event.get("body") or "{}")
order_id = f"ORD-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
table.put_item(Item={
"PK": f"ORDER#{order_id}",
"SK": "METADATA",
"orderId": order_id,
"customerId": body.get("customerId", "unknown"),
"items": body.get("items", []),
"status": "created",
"createdAt": datetime.utcnow().isoformat(),
})
return _response(201, {"orderId": order_id, "status": "created"})
# --- Get a single order ---
if method == "GET" and path.startswith("/orders/"):
order_id = event["pathParameters"]["orderId"]
result = table.get_item(Key={"PK": f"ORDER#{order_id}", "SK": "METADATA"})
item = result.get("Item")
if not item:
return _response(404, {"error": f"Order {order_id} not found"})
return _response(200, item)
return _response(200, {"message": "Testing Toolkit API",
"stage": os.environ.get("STAGE", "local")})
except Exception as e:
# π‘ Always handle exceptions. An uncaught error becomes a generic
# 500 "Internal server error" at API Gateway, hiding the real cause.
print(f"ERROR: {type(e).__name__}: {e}")
return _response(500, {"error": type(e).__name__, "detail": str(e)})
def _response(status_code, body):
return {
"statusCode": status_code,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(body, default=str),
}
π‘ The
try/exceptmatters for debugging. Without it, a failed DynamoDB call throws an uncaught exception and API Gateway returns a bareInternal server error. By catching it and returning the error name, you can see why it failed in the HTTP response itself.
Step 04: Fix the Template Routes
β οΈ The default template only maps
GET /hello. Opentemplate.yamland replace the entireResources:section'sHelloWorldFunctionso it matches your code's routes. YourHelloWorldFunctionblock should look exactly like this:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.13
Timeout: 10
Environment:
Variables:
TABLE_NAME: dev-orders
STAGE: local
Policies:
- DynamoDBCrudPolicy:
TableName: dev-orders
Events:
CreateOrder:
Type: Api
Properties:
Path: /orders
Method: post
GetOrder:
Type: Api
Properties:
Path: /orders/{orderId}
Method: get
π‘ Routes are configuration, not code. API Gateway only serves paths declared under
Events. Your function's internalif path == "/orders"logic never runs unless the template routes that path to it. This is why unknown paths return "Missing Authentication Token."π‘
DynamoDBCrudPolicyis a SAM policy template. It grants least-privilege CRUD access to just the named table, instead of a broadAmazonDynamoDBFullAccess.
Step 05: Create the Environment Variable File
β οΈ SAM local reads env vars from a JSON file. Create a file named
env.jsonin the project root (same folder astemplate.yaml) with this content:
{
"HelloWorldFunction": {
"TABLE_NAME": "dev-orders",
"STAGE": "local",
"AWS_REGION": "us-east-1"
}
}
β οΈ Windows Issue: Create this file in VS Code. Look at the bottom-right status bar. If it says "UTF-8 with BOM", click it β Save with Encoding β UTF-8. A BOM breaks JSON parsing with
Expecting value: line 1 column 1 (char 0).π‘ The top-level key is the function's logical ID.
HelloWorldFunctionmatches the resource name intemplate.yaml. PinningAWS_REGIONhere guarantees the function looks for the table in the same region you created it.
Step 06: Create a Test Event File
Create a file create_order.json inside the events/ folder :
{
"httpMethod": "POST",
"path": "/orders",
"headers": { "Content-Type": "application/json" },
"pathParameters": null,
"body": "{\"customerId\": \"CUST-001\", \"items\": [{\"productId\": \"PROD-A\", \"quantity\": 2}]}"
}
π‘ This mimics the API Gateway proxy event. When API Gateway invokes Lambda, it wraps the HTTP request in this structure.
bodyis a JSON string (escaped), not an object. That's why the code doesjson.loads(event["body"]).β οΈ Save as UTF-8 without BOM.
Step 07: Build
sam build
π‘ Always build after editing
template.yamlor your code.sam buildcopies everything into.aws-sam/build/, which is whatsam localactually runs. Skipping this means you test stale code/routes.β οΈ If
sam buildcomplains it can't find Python 3.13, runsam build --use-container(needs Docker running). It builds inside a Python 3.13 image so your local Python version doesn't matter.
Step 08: Test with sam local invoke (Direct Function Test)
This invokes the function once with your test event. No API Gateway involved.
sam local invoke HelloWorldFunction -e events/create_order.json --env-vars env.json
Expected Result: a 201 response with an orderId, and no errors.
π‘
sam local invokebypasses API Gateway. It calls the handler directly with the event you pass. Routing doesn't matter here, only the function logic and its (real) AWS calls. Because thedev-orderstable exists inus-east-1, theput_itemsucceeds.β οΈ If you see
ResourceNotFoundException, the table isn't in the region the function is using.
Step 09: Verify the item actually landed in DynamoDB:
aws dynamodb scan --table-name dev-orders --region us-east-1 --query "Items"
You should see the order you just created.
Step 10: Test with sam local start-api (Full HTTP Test)
Now test the real HTTP routing. Start the local API in one terminal:
sam local start-api --port 3000 --env-vars env.json
Leave this running.
π‘ The function logs (including real errors) print in THIS terminal. If an HTTP call returns 500, look here for the actual stack trace.
Open a second PowerShell terminal and send requests.
Step 11: Create an order (POST)
β οΈ On Windows, use
Invoke-RestMethod. PowerShell'scurlis an alias forInvoke-WebRequestwith different syntax, and inlinecurl.exeJSON escaping is painful.
Invoke-RestMethod -Uri http://localhost:3000/orders -Method Post -ContentType "application/json" -Body '{"customerId": "CUST-001", "items": [{"productId": "PROD-A", "quantity": 2}]}'
macOS / Linux / CloudShell equivalents
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d '{"customerId": "CUST-001", "items": [{"productId": "PROD-A", "quantity": 2}]}'
curl http://localhost:3000/orders/ORD-20260702093000
Expected Result: an object with orderId and status: created.
Step 12: Get that order (GET)
Copy the orderId from the previous response and:
Invoke-RestMethod -Uri http://localhost:3000/orders/ORD-20260702093000
(Replace with your actual order ID.)
Expected Result: the full order item.
orderId : ORD-20260702112404
status : created
createdAt : 2026-07-02T11:24:04.213247
PK : ORDER#ORD-20260702112404
items : {@{productId=PROD-A; quantity=2}}
customerId : CUST-001
SK : METADATA
π‘ Expected "errors" that are actually correct:
GET http://localhost:3000/β "Missing Authentication Token:" there's no root route. Fine.GET http://localhost:3000/orders(no ID) β "Missing Authentication Token:" βPOST /ordersandGET /orders/{orderId}exist. Fine.
Stop the server with Ctrl+C when done. If port 3000 stays stuck:
Get-Process -Id (Get-NetTCPConnection -LocalPort 3000).OwningProcess | Stop-Process -Force
Step 13: Unit Tests with moto (No AWS Needed)
π‘ It needs no real AWS, no Docker, no network.
motomocks AWS services in memory, so tests are fast and isolated.
pip install pytest moto boto3
β οΈ Windows Path-Length If you installed Python from the Microsoft Store, its packages live under a very long base path (
AppData\Local\Packages\PythonSoftwareFoundation...\LocalCache\...), and moto's deeply nested files push past Windows' 260-character limit, failing withOSError: [Errno 2] No such file or directory. Note thatmoto[dynamodb]does not help The extra only changes dependencies, not the files installed. Two fixes:Fix A (permanent, recommended): Enable long paths. In an Administrator PowerShell, run
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force, then reboot and re-run the install.Fix B (no admin): Use a virtual environment at a short path so the total path stays under the limit:
python -m venv C:\venvthenC:\venv\Scripts\Activate.ps1thenpip install pytest moto boto3. Don't put the venv inside the deep project folder. UseC:\venv.
Step 14: Create tests/unit/test_app.py:
import json
import os
import boto3
import pytest
from moto import mock_aws
@pytest.fixture
def dynamodb_table():
"""π‘ moto intercepts boto3 calls and simulates DynamoDB in memory.
We create the table inside the mock BEFORE importing the handler."""
with mock_aws():
os.environ["AWS_DEFAULT_REGION"] = "us-east-1"
os.environ["TABLE_NAME"] = "dev-orders"
client = boto3.resource("dynamodb", region_name="us-east-1")
client.create_table(
TableName="dev-orders",
KeySchema=[
{"AttributeName": "PK", "KeyType": "HASH"},
{"AttributeName": "SK", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "PK", "AttributeType": "S"},
{"AttributeName": "SK", "AttributeType": "S"},
],
BillingMode="PAY_PER_REQUEST",
)
yield
def test_create_order_returns_201(dynamodb_table):
# π‘ Import the handler AFTER the mock is active so its boto3
# client is intercepted by moto.
from hello_world.app import lambda_handler
event = {
"httpMethod": "POST",
"path": "/orders",
"body": json.dumps({
"customerId": "CUST-001",
"items": [{"productId": "PROD-A", "quantity": 2}],
}),
}
result = lambda_handler(event, None)
assert result["statusCode"] == 201
body = json.loads(result["body"])
assert body["status"] == "created"
assert body["orderId"].startswith("ORD-")
def test_get_missing_order_returns_404(dynamodb_table):
from hello_world.app import lambda_handler
event = {
"httpMethod": "GET",
"path": "/orders/DOESNOTEXIST",
"pathParameters": {"orderId": "DOESNOTEXIST"},
}
result = lambda_handler(event, None)
assert result["statusCode"] == 404
Step 15: Run the tests
python -m pytest tests/unit/ -v
Expected Result: both tests pass with no real AWS calls.
π‘ Unit vs Integration: these moto tests are unit tests (fast, isolated, mocked). The
Invoke-RestMethodcalls againststart-apiin Step 9 hit real DynamoDB, closer to integration tests. The difference: mock for unit tests, real deployed resources for integration tests.
Step 16: Deploy and Run Integration Tests Against a Real Stage
π‘ Unit Tests (moto) prove your logic. Integration Tests prove the whole stack works (API Gateway, Lambda, and DynamoDB together) against a real deployed environment.
sam deploy --guided
π‘
sam deployprovisions real infrastructure via CloudFormation: the Lambda, the API Gateway stage, IAM roles, and the event mappings. Unlikesam local, this creates resources that cost money and must be cleaned up.
Step 17: When it finishes, copy the API Gateway endpoint URL from the Outputs. It looks like:
https://abc123.execute-api.us-east-1.amazonaws.com/Prod/
β οΈ Note the stage is
Prod(SAM's default), so your full path is.../Prod/orders.
Step 18: Run integration tests against the deployed stage
Create tests/integration/test_api.py:
import os
import requests
# π‘ Pass the deployed URL via an environment variable so the same tests
# can run against dev, staging, or prod without code changes.
BASE_URL = os.environ["API_URL"].rstrip("/")
def test_create_and_get_order():
# Create
create = requests.post(f"{BASE_URL}/orders", json={
"customerId": "CUST-TEST-001",
"items": [{"productId": "PROD-A", "quantity": 1}],
})
assert create.status_code == 201
order_id = create.json()["orderId"]
# Retrieve the same order
get = requests.get(f"{BASE_URL}/orders/{order_id}")
assert get.status_code == 200
assert get.json()["orderId"] == order_id
def test_missing_order_returns_404():
resp = requests.get(f"{BASE_URL}/orders/DOESNOTEXIST")
assert resp.status_code == 404
Step 19: Install requests and run the tests, passing your real URL:
pip install requests
$env:API_URL = "https://abc123.execute-api.us-east-1.amazonaws.com/Prod"
python -m pytest tests/integration/ -v
Expected Result: both tests pass against live AWS.
π‘ Testing pyramid in action. Many fast unit tests (moto), fewer integration tests (deployed stage), fewest end-to-end tests. Integration tests catch problems mocks can't: IAM permissions, API Gateway mapping, real DynamoDB behaviour.
Step 20: Test an Event-Driven Flow with EventBridge
Not everything is an API. Let's test an event-driven path using the EventBridge console.
Create a rule and target (console)
Step 20.1 Open the EventBridge console β Rules β Create rule
Step 20.2: Define rule detail
-
Name:
order-placed-rule -
Event bus:
default
Click Next
Step 20.3: Build event pattern
-
Event source:
Other - Creation method: Custom pattern (JSON editor).
- Paste:
{
"source": ["orders.api"],
"detail-type": ["OrderPlaced"]
}
Click Next
Step 20.4: Select target(s)
- Target: AWS service β Lambda function
-
Target location:
Target in this current account -
Function:
testing-toolkit-HelloWorldFunction...
Click Next β Next
Step 20.5: Click Create rule
Step 21: Send a test event
Step 21.1 EventBridge console β Event buses β default β Send events
Step 21.2 Send events
-
Event source:
orders.api -
Detail type:
OrderPlaced - **Event detail:
{ "orderId": "ORD-EVT-001", "customerId": "CUST-EVT", "items": [] }
Click Send
Step 22: Verify it was delivered
Open the Lambda function β Monitor β View CloudWatch logs β newest log stream. You'll see the event was received.
π‘ Event pattern matching is exact by default.
sourceanddetail-typemust match precisely. Content-based filtering (prefix, numeric,exists) can match ondetailfields. Sending test events from the console is the fastest way to validate rules.β οΈ Our handler expects an API Gateway event shape, so it'll log the EventBridge event and likely return the default message. That's fine. The point here is proving the rule β target delivery, not the handler's response.
Step 23: Generate Realistic Test Events with SAM
Hand-writing event JSON is error-prone. sam local generate-event produces the exact structure AWS services send.
# API Gateway POST event
sam local generate-event apigateway aws-proxy --method POST --path /orders --body '{\"customerId\":\"CUST-001\"}' > events/api_post.json
# S3 object-created event
sam local generate-event s3 put --bucket my-bucket --key orders/data.json > events/s3_put.json
# SQS message event
sam local generate-event sqs receive-message > events/sqs_message.json
# EventBridge (CloudWatch) scheduled event
sam local generate-event cloudwatch scheduled-event > events/scheduled.json
List what's available:
sam local generate-event --help
sam local generate-event s3 --help
π‘ Generated events include all the metadata (request IDs, ARNs, timestamps) real triggers send, so your local tests behave like production. This is far more reliable than guessing the event shape by hand.
Troubleshooting
Symptom |
Cause | Fix |
|---|---|---|
Missing Authentication Token |
Path/method not in template Events |
Use a declared route; sam build + restart after edits |
ResourceNotFoundException |
Table missing or wrong region | Create table in the region from env.json & keep regions consistent |
Internal server error (500) |
Uncaught exception in the function | Read the start-api terminal for the real stack trace |
Expecting value: line 1 column 1 |
JSON file saved with a BOM | Save env.json/event files as UTF-8 without BOM |
Binary validation failed for python |
Python 3.13 not on PATH |
sam build --use-container or install Python 3.13 |
curl fails in PowerShell |
curl is an alias for Invoke-WebRequest |
Use Invoke-RestMethod, or curl.exe with a body file |
| Port 3000 in use | Old start-api still running |
Ctrl+C, or kill the PID on port 3000 |
β οΈ Clean Up Protocol
1 Delete the deployed stack
sam delete --stack-name testing-toolkit --region us-east-1
2 Delete the DynamoDB table
aws dynamodb delete-table --table-name dev-orders --region us-east-1
3 EventBridge β delete order-placed-rule
4 CloudWatch β delete the function's log groups
5 Remove the local testing-toolkit folder if you're done
π‘
sam deletetears down the whole CloudFormation stack, so you don't have to hunt for individual resources. The manually-created DynamoDB table isn't part of the stack, so delete it separately.**
ποΈ What You Built | π Exam Concepts Recap
| What You Built | Exam Concept |
|---|---|
| Created the table before invoking | SAM local calls real AWS, it does not mock services |
Pinned region in env.json and every command |
Region consistency across credentials, resources, and config |
Declared routes in the template Events |
API Gateway routing is configuration, not code |
Used DynamoDBCrudPolicy |
SAM policy templates for least privilege |
sam local invoke with an event file |
Direct function testing, bypassing API Gateway |
sam local start-api + HTTP calls |
Local API Gateway emulation (integration-style) |
Wrapped the handler in try/except |
Uncaught errors surface as generic 500s |
moto @mock_aws unit tests |
Mocking AWS in memory for fast, isolated unit tests |
| Imported the handler after the mock | moto must be active before boto3 clients are created |
sam deploy --guided + integration tests |
Testing against real deployed resources |
Passed the API URL via $env:API_URL |
Same tests run against any environment |
| EventBridge rule + test event | Validating event-driven delivery and pattern matching |
sam local generate-event |
Realistic event payloads for S3, SQS, API GW, etc. |
sam delete for teardown |
CloudFormation stack removal |
| Recognised "Missing Authentication Token" | Generic API Gateway "no matching route" message |
Additional Resources
- Test your serverless application with AWS SAM
- Moto: Mock AWS Servicesο
- How to test serverless functions and applications
- AWS SAM policy templates
- Lambda proxy integrations in API Gateway
ποΈ
Top comments (0)