Why Test Levels Exist and How They Fit Together
Test levels are structured stages of testing that align with how software is built and assembled. Each level has a distinct goal, scope, and typical set of defects it is best at finding. Using levels helps teams avoid two common problems: (1) testing too late, where defects become expensive to fix, and (2) testing the wrong thing at the wrong time, such as trying to validate business workflows when the underlying components are still unstable.
Think of the product as layers: small pieces of code are combined into components, components are combined into a complete system, and the system is validated against real-world usage and expectations. The classic levels are Unit, Integration, System, and Acceptance. In practice, teams may also add variations (component testing, API testing, end-to-end testing), but the core idea remains: each level answers a different question.
- Unit testing asks: “Does this small piece of code behave correctly in isolation?”
- Integration testing asks: “Do these pieces work correctly together across boundaries?”
- System testing asks: “Does the whole application behave correctly as a complete product?”
- Acceptance testing asks: “Is this ready for its intended users and environment, and does it meet acceptance criteria?”
These levels are not strictly sequential. Modern delivery often runs them continuously, but the distinction still matters because it influences test design, tooling, data setup, and what failures mean.
Unit Testing
Scope and Goal
Unit tests target the smallest testable parts of the codebase: functions, methods, classes, or modules. The goal is to verify logic and behavior in isolation, with dependencies controlled. Unit tests are typically written by developers and run frequently (often on every commit).
Unit tests are strongest at catching logic errors, edge cases, and regressions in calculations, branching, parsing, validation rules, and error handling. They are not intended to validate that the database, network, or external services work; those are integration concerns.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
Typical Characteristics
- Fast: milliseconds per test, seconds for a suite.
- Deterministic: no reliance on time, network, or shared state.
- Isolated: dependencies are replaced with fakes, stubs, or mocks.
- Precise failures: when a unit test fails, the defect is usually close to the failing code.
What to Test at Unit Level
Focus on behavior that can be validated without the full system:
- Business rules implemented in code (e.g., discount calculations, eligibility checks).
- Input validation and normalization (e.g., trimming, format checks).
- Error handling paths (e.g., null inputs, exceptions, boundary values).
- State transitions in a class (e.g., order status changes).
Avoid pushing unit tests into verifying framework behavior or trivial getters/setters unless they contain logic.
Practical Step-by-Step: Designing a Unit Test
Example scenario: a function calculates shipping cost based on weight and destination zone.
// Pseudocode example: shipping cost calculation function signature calculateShippingCost(weightKg, zone) -> money- Step 1: Identify the unit and its responsibilities. Here, the unit is calculateShippingCost, responsible for returning a cost or throwing an error for invalid input.
- Step 2: List key behaviors and edge cases. For example: weight must be > 0; zone must be one of A/B/C; boundary weights (0.1, 5.0, 5.01); rounding rules.
- Step 3: Choose representative test cases. Include normal cases and edge cases. Keep tests small and focused.
- Step 4: Control dependencies. If the function reads rates from a configuration provider, replace it with a stub that returns known rates.
- Step 5: Assert outcomes precisely. Verify returned cost, and verify that invalid inputs raise the correct error type/message (as appropriate).
// Pseudocode unit tests test_zoneA_under5kg_returnsBaseRate() test_zoneA_at5kg_returnsBaseRate() test_zoneA_over5kg_addsSurcharge() test_invalidZone_throws() test_zeroWeight_throws()Common Defects Found at Unit Level
- Off-by-one errors and boundary mistakes.
- Incorrect conditional logic (wrong branch, missing branch).
- Incorrect handling of null/empty values.
- Incorrect rounding/formatting logic.
- Regression from refactoring.
Common Pitfalls
- Over-mocking: tests become coupled to implementation details instead of behavior.
- Flaky unit tests: relying on current time, random values, or shared static state.
- Testing too much at once: large “unit” tests that behave like slow integration tests.
Integration Testing
Scope and Goal
Integration tests verify that multiple units collaborate correctly. The focus is on interfaces and boundaries: between modules, services, databases, message queues, file systems, or third-party APIs. Integration testing answers whether data is passed correctly, contracts are respected, and the combined behavior is correct.
Integration can happen at different depths:
- In-process integration: multiple modules in the same application working together.
- Service integration: one service calling another over HTTP/gRPC.
- Data integration: application interacting with a real database schema.
- Event integration: producer/consumer behavior via a queue or event bus.
What to Test at Integration Level
- API endpoints with real routing, serialization, and validation.
- Database interactions: queries, transactions, migrations, constraints.
- Inter-service contracts: request/response fields, status codes, error formats.
- Authentication/authorization integration (e.g., token validation) when it crosses components.
- Idempotency and retry behavior for network calls.
Integration tests are especially valuable for catching defects that unit tests cannot see, such as mismatched field names, incorrect data types, encoding issues, and configuration problems.
Practical Step-by-Step: API + Database Integration Test
Example scenario: an endpoint creates a customer record and stores it in the database.
// Endpoint: POST /customers // Body: { "email": "a@b.com", "name": "A" } // Behavior: stores customer, returns 201 with generated id- Step 1: Define the integration boundary. Here, it is the web layer + service layer + real database (or a containerized test database).
- Step 2: Prepare an isolated environment. Use a dedicated test database schema. Ensure tests can run in parallel without collisions (unique database per run or per test, or transactional rollback).
- Step 3: Seed required data. If the endpoint requires a valid auth token or reference data, insert or configure it as part of setup.
- Step 4: Execute the call as a client would. Send an HTTP request to the running app instance (or test server) with realistic headers and payload.
- Step 5: Assert the response. Verify status code, response body fields, and headers.
- Step 6: Assert persistence. Query the database to confirm the record exists, fields are correct, constraints are enforced, and defaults are applied.
- Step 7: Clean up. Remove created records or rollback transactions. Ensure cleanup runs even if assertions fail.
// Example assertions (conceptual) assert response.status == 201 assert response.body.id is not null assert response.body.email == "a@b.com" record = db.query("select * from customers where id = ?", response.body.id) assert record.email == "a@b.com" assert record.created_at is not nullContract and Consumer-Driven Considerations
When integrating services, failures often come from contract drift: one service changes a field name or meaning, and another service breaks. Integration tests can be strengthened by explicitly asserting the contract: required fields, optional fields, allowed values, and error formats. If your organization uses consumer-driven contract testing, treat it as a specialized integration practice: consumers define expectations, providers verify they still satisfy them.
Common Defects Found at Integration Level
- Serialization/deserialization mismatches (e.g., date formats, numeric precision).
- Incorrect database mappings, missing migrations, wrong indexes.
- Configuration errors (wrong URL, wrong credentials, missing environment variables).
- Authorization gaps across services (e.g., token accepted but scopes ignored).
- Race conditions and transaction issues.
Common Pitfalls
- Too slow or too broad: integration tests that become end-to-end suites.
- Shared environments: tests interfere with each other due to shared data.
- Unstable external dependencies: relying on third-party systems without isolation (use sandboxes or controlled test doubles where appropriate).
System Testing
Scope and Goal
System testing validates the complete, integrated application as a whole. It is typically executed in an environment that resembles production in terms of deployment topology, configuration, and supporting services. The goal is to verify that the system meets functional behavior expectations and key non-functional qualities (within the scope of system testing), such as performance characteristics, security behaviors, and reliability features.
System testing differs from integration testing mainly by breadth and realism: it exercises end-to-end flows through the full stack (UI to backend to database, or client to services to data stores) and checks that the system behaves correctly from an external viewpoint.
What to Test at System Level
- End-to-end business workflows (e.g., sign up, purchase, refund).
- Cross-cutting concerns: logging, monitoring hooks, audit trails.
- Role-based access across the application (not just one endpoint).
- Error handling and user-visible messages across the UI and APIs.
- Compatibility checks (browser/device combinations if applicable).
- Non-functional checks: basic performance smoke, security headers, rate limiting behavior, resilience patterns (where feasible).
Practical Step-by-Step: End-to-End Workflow Test
Example scenario: an e-commerce checkout flow.
- Step 1: Define the workflow boundaries. Example: user adds item to cart, applies a promo code, checks out, receives confirmation.
- Step 2: Prepare stable test data. Create a test user account, ensure product inventory exists, and define promo codes with known rules. Use unique identifiers per test run to avoid collisions.
- Step 3: Choose execution method. For UI-heavy flows, use UI automation sparingly for critical paths. For faster system tests, drive the workflow via APIs where possible and validate UI only where necessary.
- Step 4: Execute the workflow in a production-like environment. Use the same deployment artifacts and configuration style as production (feature flags, secrets management patterns, etc.).
- Step 5: Validate outcomes across layers. Confirm UI state (order confirmation page), backend state (order record stored), and side effects (email sent, event published). If validating emails/events, use test inboxes or event capture tools designed for testing.
- Step 6: Add observability checks. Confirm that expected logs/metrics/audit entries exist for the transaction, especially for regulated domains.
- Step 7: Clean up or isolate. Cancel created orders if needed, or run in an environment that can be reset.
// Example checkpoints (conceptual) assert UI shows "Order confirmed" assert order exists in database with status = "PAID" assert payment transaction id recorded assert confirmation email captured by test inbox assert inventory decremented by quantitySystem Testing and Non-Functional Coverage
System testing is often where teams first notice non-functional issues because the full stack is involved. However, not all non-functional testing belongs here. For example, deep performance testing typically requires dedicated load environments and tooling. Still, system-level suites can include “smoke” checks such as verifying that key pages load within a threshold or that an API responds within an acceptable time under minimal load.
Common Defects Found at System Level
- Broken end-to-end flows due to missing wiring or misconfigured components.
- Session and state issues (cookies, caching, CSRF tokens).
- Incorrect user permissions across screens and services.
- Unexpected behavior from combined features (feature flag interactions).
- Environment-specific defects (file paths, time zones, locale formatting).
Common Pitfalls
- Over-reliance on UI automation: slow, brittle tests that block delivery.
- Unclear ownership: failures are harder to diagnose without good logs and tracing.
- Testing too many permutations end-to-end: better to cover permutations at lower levels and keep system tests focused on critical paths.
Acceptance Testing
Scope and Goal
Acceptance testing determines whether the system is acceptable for release from the perspective of stakeholders: customers, users, business owners, operations, compliance, or other decision-makers. It is not just “more system testing.” Acceptance testing is about confirming readiness and fitness for use in the target context.
Acceptance testing often includes a mix of functional validation and operational readiness checks. It may be performed by a dedicated QA team, product owners, business analysts, end users, or a combination. In some organizations, acceptance testing is formal (sign-off required). In others, it is lightweight and continuous.
Types of Acceptance Testing (Common Variants)
- User Acceptance Testing (UAT): real users or representatives validate workflows and usability expectations.
- Operational Acceptance Testing (OAT): operations-focused checks such as backup/restore, monitoring, alerting, deployment and rollback procedures.
- Regulatory/Compliance Acceptance: evidence that required controls and auditability are in place (domain-dependent).
- Alpha/Beta acceptance: limited release to a subset of users to validate in realistic usage conditions.
What to Test at Acceptance Level
- Critical user journeys with realistic data and roles.
- Acceptance criteria for features (including edge behaviors that matter to users).
- Usability and content expectations (labels, messages, accessibility basics where applicable).
- Operational readiness: monitoring dashboards, alert thresholds, runbooks, support workflows.
- Data migration readiness (if releasing changes that affect existing data).
Practical Step-by-Step: Running a UAT Session
Example scenario: a new “Return Item” feature in an order management system.
- Step 1: Define acceptance scope and participants. Identify which user roles must approve (e.g., customer support agent, warehouse manager). Decide what “acceptable” means (pass/fail criteria, severity thresholds).
- Step 2: Prepare a UAT environment and accounts. Use a stable environment with realistic configuration. Create UAT user accounts with correct permissions and sample orders in various states (delivered, partially shipped, expired return window).
- Step 3: Provide guided scenarios. Create a short set of scripts that reflect real work: initiate return, select reason, generate label, restock item, issue refund. Include at least one negative scenario (attempt return outside window).
- Step 4: Execute and capture evidence. Participants run scenarios while recording outcomes: screenshots, timestamps, order IDs, and notes about confusing steps or missing information.
- Step 5: Triage findings immediately. Classify issues by severity and decide disposition: must-fix before release, defer with workaround, or not a defect (training/documentation gap).
- Step 6: Re-test fixes. When defects are fixed, re-run only the affected scenarios plus a small sanity set to ensure no new issues were introduced.
// Example UAT scenario checklist (abbreviated) 1) Open delivered order -> Return Item available 2) Select item and quantity -> totals update correctly 3) Choose reason -> required fields enforced 4) Submit return -> return authorization created 5) Refund issued -> correct amount and method 6) Attempt return after window -> clear message and no authorization createdPractical Step-by-Step: Operational Acceptance Checks
Acceptance often fails not because a feature is wrong, but because the system cannot be safely operated. A lightweight OAT checklist can prevent that.
- Step 1: Verify monitoring coverage. Confirm key metrics exist (error rate, latency, queue depth) and dashboards are accessible.
- Step 2: Verify alerting. Trigger a controlled failure (in a safe environment) to confirm alerts fire and reach the right channel.
- Step 3: Verify logging and traceability. Confirm that a transaction can be traced end-to-end using correlation IDs.
- Step 4: Verify backup/restore or data recovery steps (as applicable). Perform a test restore or validate that restore procedures are documented and feasible.
- Step 5: Verify deployment and rollback. Run through the deployment pipeline and confirm rollback steps work and are documented.
- Step 6: Verify access controls for operations. Ensure least-privilege access to production tools and secrets, and confirm audit logs exist where required.
Choosing the Right Level for a Test
A practical way to decide where a test belongs is to ask what you want the test to prove and what kind of failure signal you need.
- If you want fast feedback on logic and edge cases, write a unit test.
- If you want to verify boundaries (DB, HTTP, messaging, serialization), write an integration test.
- If you want confidence that a user journey works through the full stack, write a system test.
- If you want stakeholder confidence and release readiness (including operational readiness), run acceptance tests.
Also consider cost and stability. Lower-level tests are cheaper and more stable; higher-level tests are more realistic but slower and more fragile. A balanced approach is to cover most logic at unit level, cover key boundaries at integration level, keep system tests focused on critical paths, and use acceptance testing to confirm readiness and capture stakeholder feedback.
Traceability Across Levels Without Duplicating Tests
Even when you avoid duplicating the same checks at every level, you still want coverage continuity. A useful technique is to map a feature into a small set of checks per level, each with a different purpose.
- Unit: verify the core rules (e.g., refund amount calculation).
- Integration: verify persistence and service contracts (e.g., refund record stored, payment gateway request format).
- System: verify the end-to-end workflow (e.g., agent initiates refund and customer sees status update).
- Acceptance: verify the workflow meets stakeholder expectations and operational readiness (e.g., support team can handle exceptions; monitoring shows refund failures).
This approach reduces redundancy while ensuring that defects are caught as early as possible and that higher-level tests focus on what only they can validate: real integration, real workflows, and real readiness.