Overview
AWS SAM Local is the canonical local-testing toolchain for AWS Lambda. Per docs.aws.amazon.com/serverless-application-model (opens in new window), it runs Lambdas in Docker containers locally with images that mirror the Lambda runtime - same Linux, same Node/Python/Java binaries, same handler-invocation contract.
When to use
Authoring
Install
brew install aws-sam-cli
sam --version # 1.x or higher
docker --version # Required for sam localProject structure
A SAM project has template.yaml declaring Lambda functions:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
HelloFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: app.handler
Runtime: python3.12
Timeout: 10
MemorySize: 512
Events:
Api:
Type: Api
Properties:
Path: /hello
Method: GETGenerate an event payload
Per SAM docs:
sam local generate-event apigateway aws-proxy --path /hello --method GET > event.json
sam local generate-event s3 put --bucket mybucket --key file.txt > s3-event.json
sam local generate-event sqs receive-message > sqs-event.jsonSingle invocation
sam local invoke HelloFunction --event event.jsonOutput: handler's return value, plus the simulated Lambda runtime log lines.
Local API Gateway
sam local start-api --port 3000Now curl http://localhost:3000/hello exercises the full API Gateway → Lambda routing.
Local Lambda invoke endpoint
sam local start-lambda --port 3001Then point AWS SDK clients at http://localhost:3001:
import boto3
lambda_client = boto3.client('lambda', endpoint_url='http://localhost:3001', region_name='us-east-1')
lambda_client.invoke(FunctionName='HelloFunction', Payload=b'{}')Useful for testing Lambda → Lambda invocations end-to-end.
.NET Lambdas - handler-direct testing
For C#/.NET Lambda handlers, prefer in-process handler-direct invocation with Amazon.Lambda.TestUtilities (TestLambdaContext, TestLambdaLogger) over per-test sam local invoke spawns - setup, worked tests, and the dotnet-lambda CLI are in references/dotnet.md.
Integration with pytest
import subprocess, json
def invoke_lambda(name, event):
proc = subprocess.run(
["sam", "local", "invoke", name, "--event", "-"],
input=json.dumps(event), text=True, capture_output=True,
)
return json.loads(proc.stdout)
def test_hello():
result = invoke_lambda("HelloFunction", {"name": "world"})
assert result["statusCode"] == 200
assert "Hello, world" in result["body"]Running
sam build # Package Lambdas
sam local invoke HelloFunction --event event.jsonFor watch-mode:
sam build --use-container --watchCI integration
jobs:
sam-local-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: aws-actions/setup-sam@v2
- run: sam build --use-container
- run: pytest tests/lambda/Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Skip sam build between code changes | Stale package; old code runs | sam build (or watch mode) |
sam local invoke for full-suite | Spawn cost per invocation; slow | sam local start-lambda once, invoke many |
| Compare local timing to prod | Docker overhead; cold-start model differs per serverless-integration-test-builder (its cold-start budget reference) | Test correctness locally; latency in prod |
| No event-payload generation | Hand-rolled events miss fields | sam local generate-event |
| Mock AWS SDK calls locally | Tests pass but prod IAM / endpoints fail | Use LocalStack or test against real low-cost AWS account |
| Skip API Gateway routing test | Lambda alone passes; API GW integration breaks | sam local start-api |
| Hardcoded path in test | OS-specific | Use generated events |
Limitations
References
.NET Lambda testing - Amazon.Lambda.TestUtilities and the dotnet-lambda CLI
View source (opens in a new tab) (opens in new window)For C#/.NET Lambdas the fast path is handler-direct invocation with a mock context, not spawning sam local invoke per test. AWS's canonical toolkit is aws-lambda-dotnet (opens in new window): Amazon.Lambda.TestUtilities for library-level fakes plus the Mock Lambda Test Tool (opens in new window) UI for manual invocation.
Install
dotnet add package Amazon.Lambda.Core
dotnet add package Amazon.Lambda.Serialization.SystemTextJson
dotnet add package Amazon.Lambda.TestUtilities
dotnet add package Microsoft.NET.Test.Sdk
dotnet add package xunitHandler + TestLambdaContext
Per Amazon.Lambda.TestUtilities (opens in new window):
using Amazon.Lambda.Core;
using Amazon.Lambda.Serialization.SystemTextJson;
[assembly: LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]
public class Functions
{
public string Handler(string input, ILambdaContext context)
{
context.Logger.LogLine($"Got input: {input}");
return input.ToUpper();
}
}using Amazon.Lambda.TestUtilities;
using Xunit;
[Fact]
public void Handler_Uppercases()
{
var context = new TestLambdaContext
{
FunctionName = "test-fn",
RemainingTime = TimeSpan.FromSeconds(30), // Logger auto-set to TestLambdaLogger
};
Assert.Equal("HELLO", new Functions().Handler("hello", context));
}
[Fact]
public void Handler_LogsInput()
{
var context = new TestLambdaContext();
new Functions().Handler("hi", context);
var logger = (TestLambdaLogger)context.Logger;
Assert.Contains("Got input: hi", logger.Buffer.ToString());
}Remaining-time behaviour
Set RemainingTime low to exercise timeout-aware handlers (the early-return pattern in the timeout-budgets reference of serverless-integration-test-builder):
var context = new TestLambdaContext { RemainingTime = TimeSpan.FromSeconds(3) };
var result = new Functions().Handler("work-that-takes-time", context);
Assert.Contains("partial", result);Serialised event payloads
var json = File.ReadAllText("Events/apigw-request.json"); // from sam local generate-event
var request = new DefaultLambdaJsonSerializer().Deserialize<APIGatewayProxyRequest>(json);
var response = new Functions().HandleApi(request, new TestLambdaContext());
Assert.Equal(200, response.StatusCode);dotnet-lambda CLI (deploy + invoke path)
Per aws-extensions-for-dotnet-cli (opens in new window):
dotnet tool install -g Amazon.Lambda.Tools
dotnet lambda invoke-function MyFunction --payload '"hello"'For local-only testing, prefer handler-direct invocation.
CI
jobs:
dotnet-lambda-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-dotnet@v4
with: { dotnet-version: '8.0.x' }
- run: dotnet restore
- run: dotnet test --no-build --verbosity normalAnti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Spawn dotnet lambda invoke per test | Network calls; slow; rate-limited | Handler-direct invocation |
Skip TestLambdaContext | Null context fails at runtime | Always pass one |
RemainingTime = TimeSpan.MaxValue | Timeout logic never exercised | Realistic remaining time |
Hand-rolled APIGatewayProxyRequest | Schema drift | sam local generate-event fixture |
Missing [assembly: LambdaSerializer] | Runtime deserialization fails | Register the serializer in tests too |
| Mocking the Logger | Loses TestLambdaLogger.Buffer replay | Use the default TestLambdaContext logger |