Treat “func tests” as business-behavior checks for one serverless function, and treat integration tests as proof that the function works with real cloud dependencies. That split keeps your test suite fast, useful, and honest. Serverless testing gets messy quickly because the unit of deployment is small, but the real behavior depends on queues, events, permissions, storage, timeouts, retries, and provider limits.
TLDR: Use func tests to verify what a single function should do when it receives a realistic event, such as “an order-created event produces a payment request.” Use integration tests to verify that the function can talk to real services, such as SQS, DynamoDB, S3, EventBridge, API Gateway, or Pub/Sub. A team with 40 Lambda functions might run 600 func tests in under 90 seconds, then run 80 integration tests in 12 minutes before release. The best approach is not one test type, but a layered test strategy with clear ownership for each layer.
What a func test actually means
A func test, short for functional test, checks a serverless function from the outside of its handler boundary. It sends an event into the function and checks the response, side effect, or emitted message. It does not care how the function reached that result.
For example, an AWS Lambda handler may receive this event:
- An HTTP request from API Gateway
- A message from SQS
- A file notification from S3
- A scheduled event from EventBridge
- A database stream record
A func test feeds the handler a realistic version of that input. Then it checks the output. If the function validates customer data, calculates tax, and returns a 201 response, the test checks those facts. It should not inspect private methods or mock every internal call.
The value is speed. Func tests usually run locally or in CI without deploying the full stack. They are much faster than cloud-based integration tests. They also catch mistakes that unit tests miss, because they use real event shapes and real handler wiring.
Func tests vs unit tests
Unit tests check small pieces of code. They are best for pure logic: pricing rules, input parsing, permission decisions, date calculations, and retry policies. They should be quick and stable.
Func tests sit one level higher. They call the handler or function entry point. They answer a practical question: does this function behave correctly for this kind of event?
Here is the difference:
- Unit test: “Does calculateTax() return 8.25 for this state and subtotal?”
- Func test: “When the checkout Lambda receives this API request, does it return the correct total and status code?”
It drives me crazy when teams call every handler test a unit test. That hides risk. A handler test often includes serialization, headers, event parsing, validation, error mapping, and response formatting. That is functional behavior, not isolated logic.
Func tests vs integration tests
Integration tests prove that the function works with real or near-real external systems. They test the seams: identity, permissions, network calls, configuration, schemas, and service behavior.
A func test may mock DynamoDB. An integration test writes to a real DynamoDB table created for the test environment. A func test may fake an S3 event. An integration test may upload a real object to S3 and wait for the function to process it.
The difference is not academic. Many serverless failures happen outside the code:
- IAM policies are missing one action.
- An environment variable has the wrong name.
- An event source mapping uses the wrong batch size.
- A queue visibility timeout is too short.
- A database key condition works locally but fails in the cloud.
- A function times out after a cold start plus one slow API call.
Func tests will not catch all of that. Integration tests will catch much more, but they cost time and money. They can also be flaky if the test environment is shared or poorly cleaned.
Where contract tests fit
Contract tests are useful when a function consumes or produces events used by other services. They check that both sides agree on structure and meaning.
For example, a billing function may expect an event with customerId, planId, and billingPeriod. If the account service renames planId to subscriptionPlan, a contract test should fail before production breaks.
Contract tests are especially useful for event-driven systems. They reduce silent breakage. They also keep teams from relying on tribal knowledge about message formats.
Where end-to-end tests fit
End-to-end tests check a complete user or system flow. They may call a public API, trigger several functions, write to storage, publish messages, and check the final outcome.
They are valuable, but they should be few. Expect to waste time on slow failures if you build your whole safety net around end-to-end tests. One broken dependency can turn a clean CI run into a 25-minute hunt through logs, retries, and stale test data.
Use end-to-end tests for critical paths:
- User signup
- Payment creation
- Order fulfillment
- Password reset
- Data export
- Webhook processing
Do not use them to check every validation branch. That belongs in unit and func tests.
A practical testing mix for serverless functions
A serious serverless test strategy should look like this:
- Unit tests: many, very fast, focused on pure logic.
- Func tests: many, fast, focused on handler behavior and event shapes.
- Contract tests: targeted, focused on message and API compatibility.
- Integration tests: moderate number, focused on real cloud services and permissions.
- End-to-end tests: few, focused on the most valuable business flows.
A healthy ratio might be 50% unit tests, 30% func tests, 15% integration tests, and 5% end-to-end tests. The exact mix depends on risk. A payment platform needs more integration coverage than a simple notification tool.
How to write useful func tests
Good func tests use realistic events. Do not handcraft tiny objects that skip half the provider payload. Use recorded samples, official examples, or event builders that match the cloud provider format.
Each func test should answer one business question:
- Given this event and system state
- When the function runs
- Then this response, message, or side effect should happen
Mock only the edges that are not under test. If the function sends an email, replace the email provider with a fake and assert the email request. If the function queries a repository, use an in-memory repository or a controlled stub. Keep the handler path real.
Also test failure cases. Serverless systems fail in ordinary ways: duplicate events, malformed JSON, missing headers, expired tokens, throttled services, and partial batch failures. These are not rare cases. They are Tuesday afternoon.
How to write useful integration tests
Strong integration tests need clean environments. Use short-lived stacks where possible. Give each test run unique resource names or isolated tenants. Clean data after the test, even when the test fails.
Focus integration tests on risks you cannot verify locally:
- IAM access
- Event source mappings
- Queue redrive policies
- Database indexes
- Object storage notifications
- Secrets access
- Timeout and memory settings
Keep logs searchable. Include correlation IDs in test events. When a test fails, the team should find the matching cloud logs in seconds, not after digging through five consoles.
Common mistakes
The most common mistake is trusting local mocks too much. Mocks are useful, but cloud services have strict behavior. DynamoDB condition expressions, SQS batch retries, API Gateway headers, and IAM policies all have sharp edges.
Another mistake is running integration tests only after deployment to production. That is too late. Run them against a staging or preview environment first. If the function handles money, identity, security, or customer data, run the most critical checks before every release.
Finally, avoid unclear test names. test_handler_success tells nobody what failed. A better name is returns_400_when_customer_id_is_missing. Boring names save time.
Recommended approach
Use func tests as the main confidence layer for individual serverless functions. They are fast enough for every pull request and broad enough to catch handler-level bugs. Add integration tests for the parts only the cloud can prove. Add contract tests where events cross service or team boundaries. Keep end-to-end tests small and business-focused.
The goal is not to have more tests. The goal is to learn about broken behavior at the cheapest, fastest point possible. Func tests give you that early signal. Integration tests confirm the real wiring. Used together, they make serverless releases safer without turning CI into a slow, brittle mess.