End-to-End Testing APIs — Testing Complete Workflows Across Services
In this tutorial, you will learn about End. We cover key concepts, practical examples, and best practices to help you master this topic.
End-to-end (E2E) testing for APIs validates complete user workflows across all system components, from authentication through data manipulation to response verification, in a production-like environment.
What You'll Learn
Designing E2E test scenarios, multi-step workflow testing, test environment management, data seeding for E2E tests, and handling asynchronous operations.
Code Example, Practice, FAQ, Mini Project sections follow the standard template.
Code Example: Multi-Step Workflow E2E Test
import requests
import pytest
BASE = "https://staging.durga-antivirus.com"
class TestThreatManagementWorkflow:
@pytest.fixture
def session(self):
s = requests.Session()
resp = s.post(f"{BASE}/api/auth/login", json={
"username": "e2e-tester",
"password": "e2e-test-password"
})
assert resp.status_code == 200
s.headers["Authorization"] = f"Bearer {resp.json()['access_token']}"
yield s
# Cleanup: close any investigations
s.post(f"{BASE}/api/auth/logout")
def test_full_threat_management_workflow(self, session):
investigation_id = None
threat_id = None
try:
# Step 1: Create an investigation
inv_resp = session.post(f"{BASE}/api/v1/investigations", json={
"title": "E2E Test Investigation",
"priority": "high",
"description": "Automated E2E test"
})
assert inv_resp.status_code == 201
investigation_id = inv_resp.json()["id"]
# Step 2: Add threat indicators
indicator_resp = session.post(
f"{BASE}/api/v1/investigations/{investigation_id}/indicators",
json={
"type": "ip_address",
"value": "185.220.101.1",
"confidence": "high"
}
)
assert indicator_resp.status_code == 201
indicator_id = indicator_resp.json()["id"]
# Step 3: Run threat analysis
analysis_resp = session.post(
f"{BASE}/api/v1/investigations/{investigation_id}/analyze"
)
assert analysis_resp.status_code == 200
assert "threat_score" in analysis_resp.json()
# Step 4: Create threat from investigation
threat_resp = session.post(f"{BASE}/api/v1/threats", json={
"investigation_id": investigation_id,
"name": "E2E Test Threat",
"severity": "high",
"source_ip": "185.220.101.1"
})
assert threat_resp.status_code == 201
threat_id = threat_resp.json()["id"]
# Step 5: Verify threat appears in list
list_resp = session.get(f"{BASE}/api/v1/threats?severity=high")
assert list_resp.status_code == 200
threat_ids = [t["id"] for t in list_resp.json()["threats"]]
assert threat_id in threat_ids
print(f"E2E workflow completed: investigation={investigation_id}, threat={threat_id}")
finally:
# Cleanup
if threat_id:
session.delete(f"{BASE}/api/v1/threats/{threat_id}")
if investigation_id:
session.delete(f"{BASE}/api/v1/investigations/{investigation_id}")
Code Example: E2E Test for Asynchronous Operations
import time
class TestAsyncWorkflow:
def wait_for_status(self, session, url, target_status, timeout=30, interval=2):
start = time.time()
while time.time() - start < timeout:
resp = session.get(url)
assert resp.status_code == 200
if resp.json()["status"] == target_status:
return resp.json()
time.sleep(interval)
raise TimeoutError(f"Status did not change to {target_status} within {timeout}s")
def test_async_threat_scan(self, session):
# Step 1: Submit scan job
scan_resp = session.post(f"{BASE}/api/v1/scans", json={
"target": "example.com",
"scan_type": "full",
"callback_url": "https://e2e-test.example.com/callback"
})
assert scan_resp.status_code == 202
scan_id = scan_resp.json()["scan_id"]
assert scan_resp.json()["status"] == "queued"
# Step 2: Poll for completion
result = self.wait_for_status(
session,
f"{BASE}/api/v1/scans/{scan_id}",
"completed"
)
# Step 3: Verify scan results
assert "vulnerabilities" in result
assert "scan_duration" in result
assert result["scan_duration"] > 0
print(f"Async scan {scan_id} completed in {result['scan_duration']}s")
# Step 4: Verify results endpoint
results_resp = session.get(f"{BASE}/api/v1/scans/{scan_id}/results")
assert results_resp.status_code == 200
assert "findings" in results_resp.json()
Code Example: Cross-Service E2E Test
import requests
class TestCrossServiceWorkflow:
"""E2E test involving multiple API services."""
def test_threat_alert_notification_workflow(self, session):
# Step 1: Auth service — login
# Step 2: Threat service — create critical threat
threat_resp = session.post(f"{BASE}/api/v1/threats", json={
"name": "Critical E2E Test",
"severity": "critical",
"source_ip": "10.0.0.99"
})
assert threat_resp.status_code == 201
threat_id = threat_resp.json()["id"]
# Step 3: Notification service — verify alert was created
# (May need to poll for async processing)
time.sleep(2)
alerts_resp = session.get(
f"{BASE}/api/v1/alerts",
params={"threat_id": threat_id}
)
assert alerts_resp.status_code == 200
alerts = alerts_resp.json()["alerts"]
assert len(alerts) > 0
assert alerts[0]["severity"] == "critical"
assert alerts[0]["type"] == "threat_detected"
# Step 4: Notification service — verify email notification
email_resp = session.get(
f"{BASE}/api/v1/notifications",
params={"type": "email", "reference_id": threat_id}
)
email_log = email_resp.json()
assert len(email_log) > 0
assert "security-team@durga.com" in email_log[0]["recipients"]
# Cleanup
session.delete(f"{BASE}/api/v1/threats/{threat_id}")
Common Mistakes
1. Hardcoded Environment URLs
E2E tests must run against different environments (dev, staging, production). Use environment variables for base URLs, credentials, and configuration.
2. No Data Cleanup
E2E tests create real data. Always clean up test data in a finally block or use dedicated test accounts with automated cleanup scripts.
3. Flaky Timeouts
Network latency varies. Use polling with configurable timeouts and intervals. Implement exponential back-off for async operations.
4. Testing on Production
E2E tests on production risk data corruption and user disruption. Use staging environments that mirror production configuration.
5. Too Many E2E Tests
E2E tests are slow and expensive. Test only critical user journeys (login, create resource, complete workflow). Cover edge cases with integration tests.
Practice Questions
- What makes a good E2E test candidate?
- How do you handle asynchronous operations in E2E tests?
- Why should E2E tests clean up their data?
- What is the difference between E2E and integration tests?
- How do you manage test data for E2E tests?
Answers:
- Critical user journeys: user registration, payment flow, threat detection workflow. These are the paths where cross-component failures are most damaging.
- Use polling with configurable timeouts. Poll the status endpoint until it reaches the expected state. Implement a maximum wait time to prevent infinite loops.
- E2E tests create real data in shared environments. Without cleanup, data accumulates, causes test failures, and may trigger alerts or billing charges.
- E2E tests verify complete workflows across all services (auth + API + database + notification). Integration tests verify component interactions within a single service.
- Use API calls to set up test data (rather than database inserts). This validates the data creation path and ensures the test data is realistic.
Challenge: Build an E2E test suite for a threat intelligence platform covering: user login, threat creation, investigation workflow with threat analysis, alert notification, and data cleanup.
FAQ
Mini Project
Build an E2E test suite for a multi-service threat intelligence platform with three API services (auth, threats, notifications). Implement tests for complete workflows, async operation polling, cross-service verification, and automated data cleanup.
What's Next
Now learn about Postman Testing Deep Dive for API testing with the Postman collection runner.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro