Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
IT Security NachrichtenEU-Gesetz zur Cyber-Resilienz ist in Kraft - Netzpalaver(21.09.2026 um 20:29 Uhr)
Malware / Trojaner / VirenMeta Muse AI app flaw lets local malware redirect dictation traffic(21.09.2026 um 21:59 Uhr)
IT Security NachrichteniPhone 18 Pro (Max): Nutzer melden plötzliche Abstürze durch Face ID(21.09.2026 um 21:18 Uhr)
IT Security DownloadsEs wird Zeit, Word zu löschen(21.09.2026 um 21:53 Uhr)
IT NachrichtenAI can't outprompt a shortage of power, water, and land(21.09.2026 um 18:35 Uhr)
IT NachrichtenLondon neocloud Nscale takes its $1B loss to Wall Street(21.09.2026 um 19:15 Uhr)
IT Security NachrichtenEU-Gesetz zur Cyber-Resilienz ist in Kraft - Netzpalaver(21.09.2026 um 20:29 Uhr)
Malware / Trojaner / VirenMeta Muse AI app flaw lets local malware redirect dictation traffic(21.09.2026 um 21:59 Uhr)
IT Security NachrichteniPhone 18 Pro (Max): Nutzer melden plötzliche Abstürze durch Face ID(21.09.2026 um 21:18 Uhr)
IT Security DownloadsEs wird Zeit, Word zu löschen(21.09.2026 um 21:53 Uhr)
IT NachrichtenAI can't outprompt a shortage of power, water, and land(21.09.2026 um 18:35 Uhr)
IT NachrichtenLondon neocloud Nscale takes its $1B loss to Wall Street(21.09.2026 um 19:15 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Applying API Testing Frameworks: Real-World Microservices Examples

Most API testing tutorials test a single endpoint in isolation. Real systems are messier: services call each other over HTTP, share JWT tokens, and fail in ways that only show up when the whole thing runs together. This article takes a…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Most API testing tutorials test a single endpoint in isolation. Real systems are messier: services call each other over HTTP, share JWT tokens, and fail in ways that only show up when the whole thing runs together.



This article takes a different approach. We will build a small but realistic microservices system — user-service, product-service, and order-service — and then write a complete test suite for it using two different frameworks: Jest + Supertest on the Node.js side and Pytest + HTTPX on the Python side. Every code snippet in this article lives in a working repository at github.com/andre-carbajal/api-testing-microservices.









The system we are testing



Before writing a single test, it helps to understand what we are dealing with.




┌──────────────┐     JWT auth      ┌────────────────┐
│ user-service│ ◄───────────────► │ order-service │
│ :3001 │ │ :3003 │
└──────────────┘ └──────┬─────────┘
│ stock check

┌────────────────┐
│product-service │
│ :3002 │
└────────────────┘



























Service Responsibility
user-service Registration, JWT login, user profiles
product-service Product catalog, stock management
order-service Place orders; calls product-service to validate stock and decrement it


The order-service authenticates callers using the same JWT secret as the user-service — a common real-world pattern where auth is decentralized via a shared secret (or public key in production).









Two frameworks, two strategies



We are going to apply two distinct testing strategies, each matching what its framework is best suited for:






































Jest + Supertest Pytest + HTTPX
Layer Unit / in-process Integration / E2E
Services running? No — app imported directly Yes — subprocesses
HTTP mocking? Yes (order-service tests) No — real HTTP
Speed Fast (~1 s) Slower (~5–10 s)
What it catches Logic bugs, contract violations Cross-service wiring issues


Both layers are necessary. Unit tests are fast and precise; integration tests prove the system actually works end-to-end.









Part 1 — Jest + Supertest (Node.js)






Why Supertest?



Supertest wraps your Express app and lets you make real HTTP requests against it without starting a server. It binds to an ephemeral port under the hood, so your tests are fast and completely isolated from the network.




cd tests/jest && npm install









Testing the user-service



The user-service handles registration, login, and profile retrieval. Let's look at the test for registration:




// tests/jest/user-service.test.js
const request = require("supertest");
const { app, users } = require("../../services/user-service/src/index");

// Helper to register a user
const registerUser = (overrides = {}) =>
request(app)
.post("/auth/register")
.send({ name: "Alice", email: "[email protected]", password: "Pass1234!", ...overrides });

beforeEach(() => {
users.clear(); // reset in-memory state between tests
});

describe("POST /auth/register", () => {
it("creates a new user and returns 201 with user data (no password)", async () => {
const res = await registerUser();

expect(res.status).toBe(201);
expect(res.body).toMatchObject({ name: "Alice", email: "[email protected]", role: "user" });
expect(res.body).not.toHaveProperty("password"); // never expose the hash
});

it("returns 409 when email is already registered", async () => {
await registerUser();
const res = await registerUser(); // duplicate

expect(res.status).toBe(409);
expect(res.body.error).toMatch(/already registered/i);
});
});






A few things worth noting here:





  1. beforeEach(() => users.clear()) — the service exports its in-memory Map so tests can reset state without restarting the process. This is a deliberate design choice for testability.


  2. expect(res.body).not.toHaveProperty("password") — this assertion is easy to forget but critical. A regression here leaks password hashes to clients.


  3. toMatch(/already registered/i) — matching on a regex instead of an exact string makes the test resilient to minor copy changes.






Testing the login flow






describe("POST /auth/login", () => {
beforeEach(async () => {
await registerUser(); // ensure user exists
});

it("returns a JWT token on valid credentials", async () => {
const res = await request(app)
.post("/auth/login")
.send({ email: "[email protected]", password: "Pass1234!" });

expect(res.status).toBe(200);
expect(res.body).toHaveProperty("token");
expect(typeof res.body.token).toBe("string");
});

it("returns 401 on wrong password", async () => {
const res = await request(app)
.post("/auth/login")
.send({ email: "[email protected]", password: "wrongpassword" });

expect(res.status).toBe(401);
});
});









Mocking inter-service HTTP in the order-service



This is where things get interesting. The order-service calls the product-service over HTTP to validate stock and decrement it after a successful order. In unit tests, we do not want a real product-service running. We want to control exactly what it returns.



The service is designed with a seam for exactly this purpose:




// services/order-service/src/index.js (simplified)
let httpClient = null;

const getHttpClient = () => {
if (httpClient) return httpClient; // injected mock
return require("node-fetch"); // real in production
};

module.exports = { app, orders, setHttpClient };






In tests, we inject a jest.fn() that mimics the product-service responses:




// tests/jest/order-service.test.js
const { app, orders, setHttpClient } = require("../../services/order-service/src/index");

const mockFetch = (productOverrides = {}) => {
const defaultProduct = {
id: 1, name: "Mechanical Keyboard", price: 129.99, stock: 50, ...productOverrides,
};

return jest.fn().mockImplementation((url, options = {}) => {
const method = options.method || "GET";

if (method === "GET" && url.includes("/products/")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve(defaultProduct),
});
}

if (method === "PATCH" && url.includes("/stock")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ ...defaultProduct, stock: defaultProduct.stock - 2 }),
});
}
});
};

afterEach(() => {
setHttpClient(null); // restore real fetch
});






Now we can test the "insufficient stock" path without touching any real service:




it("returns 422 when stock is insufficient", async () => {
setHttpClient(mockFetch({ stock: 1 })); // only 1 unit in stock
const token = makeToken();

const res = await request(app)
.post("/orders")
.set("Authorization", `Bearer ${token}`)
.send({ items: [{ productId: 1, quantity: 5 }] }); // requesting 5

expect(res.status).toBe(422);
expect(res.body.error).toMatch(/insufficient stock/i);
});






And the happy path, verifying the total is calculated correctly:




it("creates an order and returns 201 with confirmed status", async () => {
setHttpClient(mockFetch()); // price: 129.99
const token = makeToken();

const res = await request(app)
.post("/orders")
.set("Authorization", `Bearer ${token}`)
.send({ items: [{ productId: 1, quantity: 2 }] });

expect(res.status).toBe(201);
expect(res.body).toMatchObject({
status: "confirmed",
userId: 1,
total: 259.98, // 129.99 * 2
});
});









Run the Jest suite






cd tests/jest
npm test






You should see something like:




PASS  user-service.test.js
PASS product-service.test.js
PASS order-service.test.js

Test Suites: 3 passed, 3 total
Tests: 22 passed, 22 total
Time: 1.4 s












Part 2 — Pytest + HTTPX (Python)






Why HTTPX?



HTTPX is a modern HTTP client for Python with a clean async API. Paired with pytest-asyncio, it lets you write async test functions that feel natural:




async def test_something(client):
res = await client.get("/health")
assert res.status_code == 200






Unlike the Jest tests that import the app directly, Pytest tests run against real running services. This means we need to start them first.




pip install -r tests/pytest/requirements.txt









conftest.py — shared fixtures



conftest.py is where Pytest fixtures live. Ours does three things: start the Node services as subprocesses, wait for them to be healthy, and expose async helpers for auth.




# tests/pytest/conftest.py
import subprocess, time, os
import httpx, pytest

USER_URL = os.getenv("USER_SERVICE_URL", "http://localhost:3001")
PRODUCT_URL = os.getenv("PRODUCT_SERVICE_URL", "http://localhost:3002")
ORDER_URL = os.getenv("ORDER_SERVICE_URL", "http://localhost:3003")


def _wait_for_service(url: str, retries: int = 20, delay: float = 0.5) -> None:
for _ in range(retries):
try:
r = httpx.get(f"{url}/health", timeout=2)
if r.status_code == 200:
return
except httpx.TransportError:
pass
time.sleep(delay)
raise RuntimeError(f"Service at {url} did not become healthy in time")


@pytest.fixture(scope="session")
def services():
"""Start all three Node services once per test session."""
procs = []
root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))

for rel_dir, port in [
("services/user-service", 3001),
("services/product-service", 3002),
("services/order-service", 3003),
]:
p = subprocess.Popen(
["node", "src/index.js"],
cwd=os.path.join(root, rel_dir),
env={**os.environ, "PORT": str(port)},
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
procs.append(p)

for url in [USER_URL, PRODUCT_URL, ORDER_URL]:
_wait_for_service(url)

yield {"user": USER_URL, "product": PRODUCT_URL, "order": ORDER_URL}

for p in procs:
p.terminate(); p.wait()


@pytest.fixture
async def client():
async with httpx.AsyncClient(timeout=10) as c:
yield c


@pytest.fixture
async def auth_token(client, services, registered_user):
res = await client.post(
f"{services['user']}/auth/login",
json={"email": registered_user["email"], "password": registered_user["password"]},
)
assert res.status_code == 200
return res.json()["token"]






The scope="session" on the services fixture means the services start once and stay up for the entire test run — much faster than restarting them for every test.






Integration tests for the user-service






# tests/pytest/test_user_service.py
import pytest

pytestmark = pytest.mark.asyncio


class TestRegister:
async def test_register_returns_201_with_user_data(self, client, services):
res = await client.post(
f"{services['user']}/auth/register",
json={"name": "Bob", "email": "[email protected]", "password": "PyTest1234!"},
)
assert res.status_code == 201
body = res.json()
assert body["email"] == "[email protected]"
assert body["role"] == "user"
assert "password" not in body # never expose the hash

async def test_register_returns_409_on_duplicate_email(self, client, services, registered_user):
res = await client.post(
f"{services['user']}/auth/register",
json=registered_user, # same email as the fixture
)
assert res.status_code == 409
assert "already registered" in res.json()["error"].lower()


class TestLogin:
async def test_login_returns_jwt_with_three_segments(self, client, services, registered_user):
res = await client.post(
f"{services['user']}/auth/login",
json={"email": registered_user["email"], "password": registered_user["password"]},
)
assert res.status_code == 200
token = res.json().get("token")
assert token is not None
assert len(token.split(".")) == 3 # JWTs always have three base64 segments









The crown jewel: end-to-end order flow



This test exercises the entire system in a single scenario — register, login, browse, order, verify stock, verify history:




# tests/pytest/test_order_flow.py
import pytest

pytestmark = pytest.mark.asyncio


class TestFullOrderFlow:
async def test_end_to_end_order_flow(self, client, services):
user_url, product_url, order_url = (
services["user"], services["product"], services["order"]
)

# Step 1: Register
reg_res = await client.post(
f"{user_url}/auth/register",
json={"name": "Charlie", "email": "[email protected]", "password": "E2ETest99!"},
)
assert reg_res.status_code in (201, 409)

# Step 2: Login
login_res = await client.post(
f"{user_url}/auth/login",
json={"email": "[email protected]", "password": "E2ETest99!"},
)
assert login_res.status_code == 200
token = login_res.json()["token"]
headers = {"Authorization": f"Bearer {token}"}

# Step 3: Browse products
products_res = await client.get(f"{product_url}/products")
assert products_res.status_code == 200
products = products_res.json()
item_a, item_b = products[0], products[1]
stock_before = item_a["stock"]

# Step 4: Place an order
order_res = await client.post(
f"{order_url}/orders",
headers=headers,
json={"items": [
{"productId": item_a["id"], "quantity": 2},
{"productId": item_b["id"], "quantity": 1},
]},
)
assert order_res.status_code == 201
order = order_res.json()
assert order["status"] == "confirmed"
assert len(order["items"]) == 2
assert order["total"] > 0

# Step 5: Verify stock was decremented
stock_res = await client.get(f"{product_url}/products/{item_a['id']}")
assert stock_res.json()["stock"] == stock_before - 2

# Step 6: Verify order isolation — another user cannot access this order
reg2 = await client.post(
f"{user_url}/auth/register",
json={"name": "Dave", "email": "[email protected]", "password": "E2ETest99!"},
)
login2 = await client.post(
f"{user_url}/auth/login",
json={"email": "[email protected]", "password": "E2ETest99!"},
)
headers2 = {"Authorization": f"Bearer {login2.json()['token']}"}
forbidden = await client.get(f"{order_url}/orders/{order['id']}", headers=headers2)
assert forbidden.status_code == 403






This single test would have caught the order-service stock-decrement bug, the JWT forwarding wiring, and the order isolation rule — all at once. That is the power of an end-to-end test.






Run the Pytest suite






cd tests/pytest
pytest -v






Output:




tests/pytest/test_user_service.py::TestRegister::test_register_returns_201_with_user_data PASSED
tests/pytest/test_user_service.py::TestLogin::test_login_returns_jwt_with_three_segments PASSED
tests/pytest/test_order_flow.py::TestFullOrderFlow::test_end_to_end_order_flow PASSED
tests/pytest/test_order_flow.py::TestFullOrderFlow::test_order_isolation_between_users PASSED

5 passed in 6.4s












Key patterns to take away






1. Design services for testability



Export internal state (like the in-memory users Map) so tests can reset it. Add a dependency injection seam for HTTP clients so you can mock outbound calls. These are not hacks — they are good design.




// Export state for test teardown
module.exports = { app, users };

// Export a setter so tests can inject a mock HTTP client
module.exports = { app, orders, setHttpClient };









2. Use beforeEach to reset state, not test ordering



Never rely on tests running in a specific order. Each test should set up everything it needs and clean up after itself. The beforeEach(() => users.clear()) pattern enforces this.






3. Match the right tool to the right layer





  • Supertest: fast, in-process, ideal for unit-testing Express route logic and middleware.


  • HTTPX async: clean async API for integration and E2E tests against real running services.

  • Do not try to use one tool for everything.






4. Test the contract, not the implementation



A test like expect(res.body).not.toHaveProperty("password") documents a security contract. A test that checks the exact SQL query being run is brittle and tests the wrong thing.






5. Layer your test pyramid






          ┌─────────┐
│ E2E │ ← Pytest + HTTPX (few, slow, high confidence)
┌┴─────────┴─┐
│Integration │ ← Pytest + HTTPX (services + real HTTP)
┌┴────────────┴┐
│ Unit │ ← Jest + Supertest (many, fast, isolated)
└──────────────┘






Most of your tests should be at the bottom. A handful of E2E tests at the top catch wiring issues that unit tests cannot.









Repository



All the code from this article is available at:



👉 github.com/andre-carbajal/api-testing-microservices



The repository includes:




  • Three fully working Express microservices

  • 22 Jest + Supertest unit tests

  • Pytest integration and E2E tests with async fixtures

  • A conftest.py that manages service lifecycle automatically



Clone it, run the tests, break something, and see which test catches it first.









Wrapping up



Testing APIs in a microservices architecture is not just about making requests and checking status codes. It is about choosing the right layer for each concern:




  • Use Jest + Supertest to test your route logic fast, in isolation, with full control over dependencies.

  • Use Pytest + HTTPX to prove your services wire together correctly in a real running environment.

  • Design your services with testability in mind — exportable state, injectable HTTP clients, and clean health endpoints.



The patterns here scale well: whether you have three services or thirty, the same layered approach — many fast unit tests, fewer slower integration tests, a handful of E2E smoke tests — gives you confidence without making your test suite painful to run.






Found this useful? The repo is open source — PRs and issues welcome.






1. Design services for testability



Export internal state (like the in-memory users Map) so tests can reset it. Add a dependency injection seam for HTTP clients so you can mock outbound calls. These are not hacks — they are good design.




// Export state for test teardown
module.exports = { app, users };

// Export a setter so tests can inject a mock HTTP client
module.exports = { app, orders, setHttpClient };









2. Use beforeEach to reset state, not test ordering



Never rely on tests running in a specific order. Each test should set up everything it needs and clean up after itself. The beforeEach(() => users.clear()) pattern enforces this.






3. Match the right tool to the right layer





  • Supertest: fast, in-process, ideal for unit-testing Express route logic and middleware.


  • HTTPX async: clean async API for integration and E2E tests against real running services.

  • Do not try to use one tool for everything.






4. Test the contract, not the implementation



A test like expect(res.body).not.toHaveProperty("password") documents a security contract. A test that checks the exact SQL query being run is brittle and tests the wrong thing.






5. Layer your test pyramid






          ┌─────┐
│ E2E │ ← Pytest + HTTPX (few, slow, high confidence)
┌┴─────┴┐
│Integration│ ← Pytest + HTTPX (services + real HTTP)
┌┴──────────┴┐
│ Unit │ ← Jest + Supertest (many, fast, isolated)
└────────────┘






Most of your tests should be at the bottom. A handful of E2E tests at the top catch wiring issues that unit tests cannot.









Repository



All the code from this article is available at:



👉 github.com/andre-carbajal/api-testing-microservices



The repository includes:




  • Three fully working Express microservices

  • 22 Jest + Supertest unit tests

  • Pytest integration and E2E tests with async fixtures

  • A conftest.py that manages service lifecycle automatically



Clone it, run the tests, break something, and see which test catches it first.









Wrapping up



Testing APIs in a microservices architecture is not just about making requests and checking status codes. It is about choosing the right layer for each concern:




  • Use Jest + Supertest to test your route logic fast, in isolation, with full control over dependencies.

  • Use Pytest + HTTPX to prove your services wire together correctly in a real running environment.

  • Design your services with testability in mind — exportable state, injectable HTTP clients, and clean health endpoints.



The patterns here scale well: whether you have three services or thirty, the same layered approach — many fast unit tests, fewer slower integration tests, a handful of E2E smoke tests — gives you confidence without making your test suite painful to run.






Found this useful? The repo is open source — PRs and issues welcome.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Applying API Testing Frameworks: Real-World Microservices Examples

Thematisch verwandte Begriffe: Applying, Testing, Frameworks, RealWorld · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-94494 | jshERP through 3.6 contains a tenant isolation bypass vulnerability that…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick