Testing Applications in AWS Development Environments
Testing applications in development environments means exercising your code against emulated or real AWS services before it reaches production — locally with the SAM CLI, against dedicated API Gateway stages, and in dev copies of your CloudFormation stacks. Task 3.2 of the DVA-C02 exam covers the developer's inner loop: invoking functions with sam local invoke, emulating an API with sam local start-api, choosing between mocked AWS calls and real dev-stage resources, updating an existing stack safely with change sets, and iterating in seconds with sam sync. Version 2.1 of the exam guide added event-driven testing, so expect questions on generating and injecting test events, validating EventBridge event patterns, and verifying asynchronous outcomes you cannot assert on directly. This lesson gives you the decision rules the exam rewards: what to mock, when a real environment is worth the setup cost, how to preview a stack update before it applies, and how to prove an async pipeline actually ran.
On this page8 sections
- Testing code with AWS services and tools
- Unit tests versus integration tests: what to mock and when
- Mocking external APIs and dependencies
- API Gateway stages as development endpoints
- Deploying stack updates to existing environments
- Rapid iteration with sam sync and SAM Accelerate
- Testing event-driven applications
- Scenario: taking an order pipeline through dev testing
- Test functions locally with sam local invoke, start-api, and start-lambda, and know what each emulates
- Choose between unit tests with mocked AWS calls, mock API endpoints, and integration tests against real dev-stage resources
- Configure and invoke API Gateway stages as isolated development, test, and production endpoints on one API
- Deploy stack updates to an existing environment with sam deploy, parameter overrides, and change-set previews
- Iterate rapidly against a dev stack with sam sync (SAM Accelerate) and know why it is development-only
- Test event-driven applications by injecting events, validating EventBridge patterns, and verifying asynchronous outcomes with correlation IDs and polling
Testing code with AWS services and tools
Testing deployed and pre-deployment code on AWS revolves around a small toolkit the exam names explicitly, led by the AWS SAM CLI's local commands. All of them run your function inside a Docker container that simulates the Lambda runtime, so Docker must be installed locally.
| Command | What it emulates | When to use it |
|---|---|---|
sam local invoke | One Lambda invocation with an event payload (-e event.json) | Quick single-shot test of one function's logic |
sam local start-api | API Gateway + your functions as a local HTTP endpoint | Exercising REST routes with curl or a browser during development |
sam local start-lambda | The Lambda invoke API as a local endpoint | Letting automated test code or the AWS CLI/SDK invoke functions as if they were deployed |
sam local generate-event | Sample event payloads (S3, SQS, API Gateway, and more) | Producing realistic test events without writing JSON by hand |
The distinction the exam probes: start-api is for calling your HTTP routes; start-lambda is for pointing an SDK or test framework at a local Lambda endpoint.
Beyond local emulation, the Lambda console lets you configure saved test events and invoke a deployed function directly, inspecting the response and logs in place — and test events can be shared so a team tests with the same payloads. AWS CloudShell gives you a browser-based, pre-authenticated shell with the AWS CLI ready, useful for poking deployed resources without local credential setup. Local emulation is fast but approximate; the deployed function is the truth.
Unit tests versus integration tests: what to mock and when
Unit tests and integration tests answer different questions, and Skill 3.2.2 expects you to pick the right one for a scenario. A unit test verifies your code's logic in isolation: every AWS SDK call and external API is replaced with a mock or stub, so tests run in milliseconds, cost nothing, need no credentials, and can simulate hard-to-produce conditions like throttling errors. An integration test verifies that your code works with the real services: it runs against actual resources in a dev or test stack, catching everything mocks hide — IAM permission gaps, wrong resource names, serialization mismatches, service behavior you misremembered.
| Unit (mocked SDK) | Integration (real dev resources) | |
|---|---|---|
| Speed and cost | Milliseconds, free | Seconds to minutes, real AWS usage |
| Catches | Logic bugs, error-path handling | IAM, configuration, and service-behavior bugs |
| Needs | No credentials, no network | A deployed dev stack and credentials |
| Blind spot | Your assumptions about AWS are baked into the mock | Slower feedback; shared-environment interference |
The decision rule: mock at the unit level for fast feedback on logic, then run a smaller set of integration tests against real dev-stage resources before promoting. A test suite that only mocks proves nothing about permissions; a suite that only hits real services is too slow to run on every change. On the exam, "tests pass locally but the deployed function gets AccessDenied" signals that mocked tests never exercised IAM — the fix is integration testing in a dev environment, not more mocks.
Mocking external APIs and dependencies
External dependencies — a partner's REST API, a payment provider, a rate-limited third-party service — are the classic reason to mock even in integration-style tests: you cannot control their responses, you may be billed per call, and their sandbox may not exist. Skill 3.2.2 calls this out directly: write integration tests and mock APIs for external dependencies.
The main techniques:
- Mock the client in code. Inject the HTTP client or SDK wrapper and substitute a fake in tests. This is the unit-test approach extended to third parties, and it is where you simulate failure modes — timeouts, 429s, malformed bodies — that the real service will not produce on demand.
- Stand up a mock endpoint. Run a local stub server, or use an API Gateway mock integration: a method that returns a configured response directly from API Gateway without any backend. Mock integrations let a dependent team develop and test against your API's contract before the real backend exists, and they cost almost nothing to serve.
- Point config at the mock. Because the endpoint URL is environment-specific configuration (an environment variable or parameter — never hardcoded), the dev environment can target the mock while production targets the real service, with no code change.
The pattern to recognize on the exam: "the backend service is not yet available, but the team needs to develop against the API" is an API Gateway mock integration; "simulate the third-party API returning errors" is a code-level mock or stub server. Either way, the switch between mock and real endpoint is configuration, which is why the config-outside-the-artifact principle from Task 3.1 matters here too.
API Gateway stages as development endpoints
An API Gateway stage is a named, independently invocable deployment of your API — and one API can carry several, typically dev, test, and prod. Each stage has its own invoke URL, with the stage name as the path prefix: https://abc123.execute-api.us-east-1.amazonaws.com/dev/orders versus .../prod/orders. Calling a specific stage is simply calling its URL, so a developer can hit dev all day without touching production traffic.
Stages isolate more than routing. Each stage carries its own settings: logging and tracing configuration, throttling limits, caching, and stage variables — key-value pairs available to the integration, commonly used to point each stage at a different backend (for example, a Lambda alias or a per-environment endpoint). That makes one API definition serve as many environments, each configured appropriately: verbose logging and no cache in dev, production throttles and caching in prod.
The deployment model matters for the exam: editing API resources or methods changes only your working definition. Nothing a client sees changes until you deploy the API to a stage. That is both the isolation mechanism and a classic troubleshooting question — "we updated the API but the dev endpoint still returns the old behavior" means nobody deployed the changes to the dev stage.
The workflow this enables: deploy work-in-progress to dev, run integration tests against the dev invoke URL, promote the same API definition to test for wider verification, and deploy to prod only when it passes. Creating and managing those stage environments in CI belongs to Task 3.3; here, the skill is using a stage as your development endpoint.
Deploying stack updates to existing environments
Development testing constantly redeploys, so Skill 3.2.4 covers updating a stack that already exists. When you run sam deploy against an existing stack, CloudFormation performs an update: it diffs the submitted template against the stack's current state and changes only what differs — updating some resources in place and, for certain property changes, replacing them entirely. If an update fails, CloudFormation rolls the stack back to its previous working state.
Deploying the same template to a different environment is a matter of stack name and parameters: sam deploy --stack-name orders-api-staging --parameter-overrides Stage=staging TableName=orders-staging creates or updates the staging stack without touching dev. samconfig.toml makes this repeatable by storing named configuration environments — deploy parameters for each target — selected with --config-env staging, so "deploy a SAM template to a different staging environment" (the guide's own example) is one command, not a memorized argument list.
Change sets are the safety mechanism. A change set is CloudFormation's preview: it lists every resource the update would add, modify, or replace — before anything happens. sam deploy creates one and, by default, asks for confirmation; --no-execute-changeset creates the preview without applying it, which is how you check that an innocent-looking template edit will not replace your database. Reviewing a change set is the difference between "update the stack and hope" and knowing the blast radius in advance.
The exam angle: updating an existing environment is normal and safe when parameterized per environment and previewed with change sets — recreating stacks from scratch for each change is the distractor.
Rapid iteration with sam sync and SAM Accelerate
The full sam build → sam deploy → CloudFormation update cycle is correct but slow for the inner development loop — waiting a minute to see a one-line change is where dev velocity dies. AWS SAM Accelerate, driven by the sam sync command, exists to shrink that loop.
sam sync --watch observes your project and pushes changes to your dev stack as you save. Its key optimization is distinguishing change types: for code-only changes — a Lambda function body, for instance — it skips the CloudFormation update entirely and pushes the new code straight to the resource via service APIs, landing in seconds. For infrastructure changes to the template, it falls back to a proper CloudFormation deployment, keeping the stack definition authoritative.
That shortcut is also exactly why sam sync is for development environments only. Bypassing CloudFormation means the deployed resource temporarily reflects your working directory rather than a versioned, reviewed template — acceptable when the audience is you, unacceptable for staging or production, where every change should flow through the pipeline and leave an auditable trail. Treat the dev stack synced this way as disposable.
The workflow this enables is testing against real cloud resources at near-local speed: your function runs with its actual IAM role, talks to the real dev DynamoDB table, and emits real logs — the fidelity local Docker emulation cannot provide — while feedback stays fast enough for tight iteration. On the exam, "the developer wants to see code changes reflected in the development stack in seconds" points at sam sync --watch; anything mentioning production points away from it.
Testing event-driven applications
Event-driven testing is the v2.1 addition to this task, and it is harder than request/response testing for one reason: there is no response to assert on. An event goes in, and the outcome happens later, elsewhere — a message lands on a queue, a function fires, a table row appears.
Generating and injecting events comes first. sam local generate-event produces realistic payloads for dozens of sources — sam local generate-event s3 put, for example — which you feed to sam local invoke -e or save as console test events. Against a deployed dev stack, you inject the real thing: aws events put-events publishes a custom event to an EventBridge bus, and the actual rules, targets, and permissions process it.
Validating routing before sending: EventBridge's TestEventPattern operation checks whether a given event matches a given event pattern — so you can verify your rule's pattern against sample events programmatically, catching a pattern that silently matches nothing before you spend an afternoon wondering why a target never fires.
Verifying asynchronous outcomes requires observing target state rather than a return value. The standard patterns:
- Correlation IDs — stamp each test event with a unique ID that every downstream component logs and propagates, so you can trace one event's journey through CloudWatch Logs and distinguish your test from other traffic.
- Poll target state — the test injects the event, then polls the observable end state (a message on an SQS queue subscribed for testing, an item in the table, a log entry containing the correlation ID) with a timeout. Assert on arrival within the window; fail if it never comes.
- Check the failure paths — dead-letter queues catch events that failed processing; an empty DLQ after the test is itself an assertion.
The anti-pattern is asserting immediately after injecting — async means eventually, and a fixed sleep is a flaky compromise; poll with a deadline instead.
Scenario: taking an order pipeline through dev testing
Put the whole task together. You are building an order pipeline: API Gateway → Lambda (CreateOrder) → EventBridge → Lambda (ReserveInventory) → DynamoDB, defined in a SAM template.
Local first. You unit-test both functions with mocked SDK clients, then sam local generate-event apigateway aws-proxy to produce a realistic request and sam local invoke CreateOrder -e event.json to run the handler in Docker. The logic works; nothing yet proves the wiring does.
Deploy the dev stack. sam deploy --config-env dev updates your existing dev stack — the change set shows only the two functions and one new EventBridge rule changing, so you execute it. You start sam sync --watch for the rest of the session, so each code tweak lands in seconds.
Test through the dev endpoint. You call the dev stage URL with a test order. The response is fine — but did the event flow? First you validate the rule's pattern with TestEventPattern against a sample of the exact event CreateOrder publishes: it matches.
Verify the async leg. Your integration test posts an order carrying a correlation ID, then polls the dev DynamoDB table for the reservation item with a 30-second deadline. It never appears. The correlation ID turns up in CreateOrder's logs but never in ReserveInventory's — the event was published to the bus, matched the rule, and then failed on the target's permissions. An AccessDenied your mocked unit tests could never see, found in minutes because you tested against real dev resources and could trace one event end to end. You fix the role, the poll passes, and the change is ready to promote.
Tip. This task appears as tooling-selection and debugging scenarios. Expect to pick the right sam local command from a description — "invoke the function locally with a sample S3 event" (generate-event + invoke) versus "test the REST endpoints locally" (start-api) versus "let the test suite call functions through the SDK" (start-lambda). "Tests pass locally but the deployed function fails with AccessDenied" signals mocked unit tests needing real integration tests in a dev environment. Trigger phrases: "preview the changes before updating the stack" (change sets), "see code changes in the dev stack within seconds" (sam sync --watch), "verify the event pattern matches" (TestEventPattern), and "the API changes aren't visible" (not yet deployed to the stage). Event-driven questions reward correlation IDs and polling target state over sleeps or direct assertions.
- sam local invoke runs one function in Docker; sam local start-api emulates API Gateway for HTTP calls; sam local start-lambda exposes the Lambda invoke API to SDKs and test code.
- Unit tests mock AWS SDK calls for fast logic feedback; integration tests against real dev-stage resources catch what mocks cannot — IAM, configuration, and service behavior.
- API Gateway mock integrations return configured responses with no backend — use them to develop against an API whose real backend does not exist yet.
- One API Gateway API hosts multiple stages (dev/test/prod), each with its own invoke URL, settings, and stage variables — and changes are invisible until deployed to a stage.
- sam deploy to an existing stack is a CloudFormation update; preview it with a change set (--no-execute-changeset) to see adds, modifies, and replacements before they happen.
- sam sync --watch (SAM Accelerate) pushes code-only changes to a dev stack in seconds by bypassing CloudFormation — development environments only, never production.
- Validate an EventBridge rule with TestEventPattern before injecting events with put-events.
- Async outcomes cannot be asserted directly: stamp events with correlation IDs, then poll the target state (queue, table, logs) with a timeout instead of sleeping.
Frequently asked questions
What is the difference between sam local invoke and sam local start-api?
sam local invoke runs a single Lambda function once, in a local Docker container, with an event payload you supply — ideal for testing one handler's logic. sam local start-api emulates your API Gateway routes as a local HTTP endpoint, so you exercise the API with curl or a browser and it invokes your functions per request. Use invoke for one function, start-api for the HTTP-facing flow.
How do I test a Lambda function without deploying it?
Use the AWS SAM CLI with Docker installed: generate a realistic payload with sam local generate-event, then run sam local invoke -e event.json to execute the function locally in a Lambda-like container. For automated tests, sam local start-lambda exposes a local endpoint that the AWS SDK or CLI can invoke as if the function were deployed. Local emulation approximates the real runtime — still run integration tests against a deployed dev stack before promoting.
What are API Gateway stages used for in testing?
Stages give one API several isolated, independently configured deployments — commonly dev, test, and prod — each with its own invoke URL (the stage name is the path prefix), its own logging, throttling, and caching settings, and its own stage variables for pointing at per-environment backends. You test against the dev stage's URL without any risk to production, and no change is live on a stage until the API is deployed to it.
What does sam sync do and when should I use it?
sam sync (AWS SAM Accelerate) rapidly synchronizes your local project with a deployed stack. With --watch it detects saves and pushes code-only changes directly to resources in seconds, bypassing the full CloudFormation update; template changes still go through CloudFormation. Use it for fast iteration against a development stack — never for staging or production, where every change should flow through a reviewed, auditable pipeline deployment.
How do you test event-driven applications on AWS?
In three moves: generate realistic events (sam local generate-event, or custom payloads) and inject them — locally via sam local invoke, or into a deployed dev stack with aws events put-events; validate that EventBridge rule patterns actually match your events using the TestEventPattern operation; and verify outcomes asynchronously, because there is no direct response — tag events with a correlation ID, then poll the observable target state (an SQS queue, a DynamoDB item, a log entry) with a timeout, and check dead-letter queues for failures.
What is a CloudFormation change set and why use one before updating a dev stack?
A change set is a preview of a stack update: CloudFormation compares the new template and parameters against the running stack and lists every resource it would add, modify, or replace — without applying anything. Reviewing it (sam deploy --no-execute-changeset, or the confirmation prompt) tells you the blast radius before you commit, most importantly whether a change forces resource replacement, such as of a database.
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.