aws-sam-local-testing

Use when AWS Lambda functions with API Gateway need testing locally: sam local invoke with event payloads, start-api for a local API Gateway, start-lambda for SDK clients and generate-event, plus .NET Lambda handler tests with Amazon.Lambda.TestUtilities.

/plugin install qa-serverless@testland-qa

First time in Claude Code? Add the marketplace with /plugin marketplace add testland/qa

View source (opens in a new tab)

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

  • Local unit + integration tests for Lambda handlers.
  • Testing API Gateway + Lambda routing locally.
  • Reproducing prod Lambda behaviour without deploying.
  • Event-payload-driven tests (S3 event, SQS message, API GW request).

Authoring

Install

brew install aws-sam-cli
sam --version            # 1.x or higher
docker --version         # Required for sam local

Project 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: GET

Generate 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.json

Single invocation

sam local invoke HelloFunction --event event.json

Output: handler's return value, plus the simulated Lambda runtime log lines.

Local API Gateway

sam local start-api --port 3000

Now curl http://localhost:3000/hello exercises the full API Gateway → Lambda routing.

Local Lambda invoke endpoint

sam local start-lambda --port 3001

Then 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.json

For watch-mode:

sam build --use-container --watch

CI 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-patternWhy it failsFix
Skip sam build between code changesStale package; old code runssam build (or watch mode)
sam local invoke for full-suiteSpawn cost per invocation; slowsam local start-lambda once, invoke many
Compare local timing to prodDocker overhead; cold-start model differs per serverless-integration-test-builder (its cold-start budget reference)Test correctness locally; latency in prod
No event-payload generationHand-rolled events miss fieldssam local generate-event
Mock AWS SDK calls locallyTests pass but prod IAM / endpoints failUse LocalStack or test against real low-cost AWS account
Skip API Gateway routing testLambda alone passes; API GW integration breakssam local start-api
Hardcoded path in testOS-specificUse generated events

Limitations

  • Docker overhead. Cold starts in SAM Local are 5-15s (Docker container spin-up); not representative of prod cold-start budgets per serverless-integration-test-builder (its cold-start budget reference).
  • Doesn't test IAM. Local invocations run with your AWS CLI credentials, not the Lambda's role.
  • Doesn't test event-source mapping. SQS / DynamoDB Streams / EventBridge bindings are SAM-template-only locally.
  • VPC + private endpoints can't be simulated. Local Lambdas reach internet directly.
  • Pair with LocalStack for fuller AWS-service emulation (localstack.cloud (opens in new window)).

References

Reference

.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 xunit

Handler + 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 normal

Anti-patterns

Anti-patternWhy it failsFix
Spawn dotnet lambda invoke per testNetwork calls; slow; rate-limitedHandler-direct invocation
Skip TestLambdaContextNull context fails at runtimeAlways pass one
RemainingTime = TimeSpan.MaxValueTimeout logic never exercisedRealistic remaining time
Hand-rolled APIGatewayProxyRequestSchema driftsam local generate-event fixture
Missing [assembly: LambdaSerializer]Runtime deserialization failsRegister the serializer in tests too
Mocking the LoggerLoses TestLambdaLogger.Buffer replayUse the default TestLambdaContext logger

Limitations

  • In-process tests use the standard JIT; ReadyToRun / Native AOT Lambdas behave differently - pair with deployed-Lambda tests.
  • The Lambda runtime API (next/response polling) is not exercised.
  • Cold-start behaviour is invisible warm-in-process; see the parent skill's budget tables.

References