Software Testing Types: Unit, Integration, Regression, Smoke, E2E, and System Tests
Research date: Aug 22, 2026. This is a synthesis of standard software-testing terminology applied to a concrete service shape (FastAPI reading SQL Server, transforming with pandas, streaming pyarrow back to an HTTP client). The taxonomy is conventional; the scenario mapping is the useful part.
TL;DR
- Unit test — one function/class in isolation, dependencies mocked, synthetic input, no I/O. Fast, cheap, run on every commit.
- Integration test — two or more real components wired together (route + pandas + SQL Server). Slower, needs the real dependency.
- Regression test — not a separate layer; a test whose purpose is to catch a previously-fixed bug coming back. Any test type can be a regression test.
- Smoke test — a shallow, fast sanity pass over the critical path after a deploy. Usually a thin subset of E2E.
- End-to-end (E2E) test — the full system exercised over the real interface, from a real client process to a real server process.
- System test — the whole deployed product validated as one black box against requirements in an environment close to production (UAT): performance, security, configuration, connectivity.
Scope ladder: Unit < Integration < System < E2E. Smoke is a lightweight subset of E2E. Regression is a goal, not a layer.
1. The six types at a glance
| Type | Scope | Real I/O? | Speed | Catches |
|---|---|---|---|---|
| Unit | One function/class | No (mocked) | ms | Wrong logic, edge cases in a pure function |
| Integration | Several units together | Yes (DB, services) | s | Wiring, contract, schema mismatch |
| Regression | Any of the above, re-run | Depends | Depends | Old bugs resurfacing |
| Smoke | Critical path, post-deploy | Yes | s–min | Server boots, main route answers |
| End-to-end | Full stack via real interface | Yes | min | Cross-process, protocol, serialization issues |
| System | Whole deployed system vs requirements | Yes (real env) | min–hr | Deployment, config, perf, non-functional requirements |
The useful mental model is a pyramid: many unit tests at the base, fewer integration tests, a small handful of E2E/system tests at the top. Smoke tests sit at the top too but are run after every deploy as a first gate, not as part of the regular test suite.
Regression deserves its own note
Regression is the most commonly mislabeled term. A regression test is any test you add (or keep) specifically so a fixed bug does not come back. The same test can be simultaneously a unit test, an integration test, and a regression test. "Regression" describes intent, the other labels describe scope.
2. The concrete scenario
A FastAPI HTTP server that:
- reads rows from SQL Server,
- transforms them with a pandas pipeline,
- returns the result as a pyarrow stream to an HTTP client.
The four test cases, mapped to types:
Test case 1 — synthesized in-memory dataframe for the pandas pipeline
Type: Unit test.
Feed the pipeline function a hand-built pandas.DataFrame (no DB, no HTTP). Assert on the transformed output. This is the fastest, most valuable test: pandas transformations are pure-ish functions, so they get full coverage here. Run it on every commit and in CI on every push.
Test case 2 — FastAPI TestClient calling a route against the real SQL Server
Type: Integration test.
TestClient runs the ASGI app in-process (no real network socket), but the route still hits the actual SQL Server and does the real pandas work. This verifies the wiring: route → query → pandas → response encoding. It does not verify the HTTP layer, TCP, or the pyarrow framing on the wire, because TestClient bypasses the real transport.
Test case 3 — server subprocess + separate client process hitting it
Type: End-to-end test. (Also qualifies as a smoke test if it only checks the critical path.)
Spin up the FastAPI server with uvicorn as a real subprocess, then run a separate client process that calls it over a real TCP socket and decodes the actual pyarrow stream. This is the first test that catches everything TestClient hides: real HTTP, real streaming/chunked encoding, real pyarrow IPC framing, real server lifecycle. Nothing is mocked except external systems you cannot run locally.
Test case 4 — deployed to UAT, Python client connects remotely
Type: System test.
The service is deployed to UAT and a Python client connects to the remote host. This validates the deployment, not just the code: config, secrets, firewall, DNS, DB credentials in the target environment, TLS, and realistic latency/throughput. If it includes load/performance checks against requirements, that is system testing in the strict sense.
3. Why the mapping matters
Each type is a rung with a different confidence/cost tradeoff:
- Unit tests find logic bugs fast and cheaply, but can be wrong about reality (a mocked DB returns clean rows; the real one returns
Noneand weird types). - Integration tests close the mock/reality gap for the components you wired together.
- E2E tests close the transport gap (streaming, serialization, process lifecycle).
- System tests close the environment gap (deployment, config, connectivity).
You cannot skip the bottom rungs just because you have E2E: E2E is slow and hard to debug, so it must be pointed at a few critical journeys, not every edge case. And you cannot skip the top rungs just because units pass: units never catch "the route 500s because the response model rejects the pyarrow type."
For this service, the high-risk spots are the integration boundary (SQL Server types → pandas dtypes) and the streaming boundary (pandas → pyarrow IPC → HTTP response). So the strategy is: dense unit tests on the pipeline, a solid integration suite against a real (or containerized) SQL Server, a single E2E streaming test, and one smoke check run against every UAT deployment.
4. A small pytest sketch for each layer
# 1. Unit — pure pipeline, no I/O
def test_transform_drops_missing_rows():
df = pd.DataFrame({"a": [1, None, 3]})
assert transform(df).shape == (2, 1)
# 2. Integration — TestClient, real SQL Server
client = TestClient(app)
def test_route_returns_pyarrow():
resp = client.get("/data")
assert resp.status_code == 200
table = pa.ipc.open_stream(resp.content).read_all()
assert table.num_rows > 0
# 3. E2E — real server subprocess, real client process
server = subprocess.Popen(["uvicorn", "app:app", "--port", "8010"])
# separate client process connects over TCP, decodes the stream
client_script = ["python", "e2e_client.py", "--port", "8010"]
assert subprocess.run(client_script, check=True).returncode == 0
# 4. System — run against UAT, gated by env var
# pytest -m system --uat-base-url https://uat.example.com
@pytest.mark.system
def test_remote_stream(uat_base_url):
table = fetch_stream(f"{uat_base_url}/data")
assert table.schema.equals(expected_schema)
5. Key takeaways
- Label tests by scope (what they touch), not by framework.
TestClientis an integration test, not a unit test, the moment it hits a real DB. - Regression is intent: add a test for every bug you fix, and it becomes a permanent regression guard.
- Smoke and E2E differ by depth and timing: smoke is the shallow post-deploy sanity gate, E2E is the full-stack verification.
- System tests are about the environment as much as the code — they are the only ones that catch UAT-specific config and connectivity problems.
- Match the test count to the pyramid: many units, fewer integrations, a few E2E, one smoke per deploy.