Skip to content

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:

  1. reads rows from SQL Server,
  2. transforms them with a pandas pipeline,
  3. 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 None and 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

  1. Label tests by scope (what they touch), not by framework. TestClient is an integration test, not a unit test, the moment it hits a real DB.
  2. Regression is intent: add a test for every bug you fix, and it becomes a permanent regression guard.
  3. Smoke and E2E differ by depth and timing: smoke is the shallow post-deploy sanity gate, E2E is the full-stack verification.
  4. System tests are about the environment as much as the code — they are the only ones that catch UAT-specific config and connectivity problems.
  5. Match the test count to the pyramid: many units, fewer integrations, a few E2E, one smoke per deploy.