10 Types of API Testing Every SDET Should Master (With Code Examples)
A developer pushed a new user registration endpoint to production on a Friday afternoon. The endpoint accepted POST requests, returned a 201 status code with a user object, and passed the team’s 15 API tests. Ship it.
Monday morning: 400 duplicate user accounts, a crashed email notification service, and a security report showing the endpoint accepted SQL injection in the username field. The 15 tests covered functional validation. Nobody had tested for idempotency, load handling, security vulnerabilities, or error propagation to downstream services.
API testing isn’t one thing. It’s at least ten things, and most teams only do two or three of them.
Contents
1. Functional Testing
This is where every team starts — and where most teams stop. Functional testing verifies that API endpoints do what they’re supposed to: accept the right inputs, return the right outputs, and handle basic error cases.
For a user registration endpoint, functional tests verify: valid registration returns 201 with a user object, duplicate email returns 409, missing required fields return 400 with descriptive error messages, and the response body matches the documented schema.
import io.restassured.RestAssured;
import org.junit.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
public class UserRegistrationFunctionalTests {
@Test
public void testSuccessfulRegistration() {
given()
.contentType("application/json")
.body("{"email":"new@test.com","password":"Pass123!","name":"Test User"}")
.when()
.post("/api/users")
.then()
.statusCode(201)
.body("id", notNullValue())
.body("email", equalTo("new@test.com"))
.body("name", equalTo("Test User"))
.body("password", nullValue()); // Password should NOT be returned
}
@Test
public void testDuplicateEmailReturns409() {
given()
.contentType("application/json")
.body("{"email":"existing@test.com","password":"Pass123!","name":"Dup User"}")
.when()
.post("/api/users")
.then()
.statusCode(409)
.body("error", containsString("already exists"));
}
}
When to use: Every API. This is your baseline. Without functional tests, nothing else matters.
2. Contract Testing
Contract testing verifies that the API’s response structure matches what consumers expect. When your frontend team expects {"user": {"id": 1, "name": "John"}} and the backend returns {"data": {"userId": 1, "fullName": "John"}}, contract tests catch this before production.
Tools like Pact enable consumer-driven contract testing, where the frontend defines the contract it expects, and the backend verifies it can fulfill that contract. This is especially critical in microservice architectures where dozens of services communicate through APIs.
When to use: Any API consumed by multiple clients (web, mobile, partner integrations) or in microservice architectures where services evolve independently.
3. Integration Testing
Integration tests verify that the API works correctly with its dependencies: databases, message queues, external services, and caches. A functional test might mock the database. An integration test uses the real database to verify that data persists correctly, transactions commit, and queries return expected results.
The user registration endpoint might pass functional tests with a mocked database but fail in integration when the email column has a uniqueness constraint the mock doesn’t enforce, or when the database connection pool is exhausted under load.
When to use: Any API that interacts with databases, external services, or other APIs. Critical for catching issues that mocks hide.
4. Security Testing
Security testing probes APIs for vulnerabilities: SQL injection, cross-site scripting (XSS), authentication bypass, authorization flaws, and data exposure. This is the type of testing most frequently skipped — and most frequently responsible for data breaches.
# Security test examples with Python requests
import requests
BASE_URL = "https://api.example.com"
def test_sql_injection_in_search():
"""Verify search endpoint isn't vulnerable to SQL injection"""
payloads = [
"' OR '1'='1",
"'; DROP TABLE users; --",
"' UNION SELECT * FROM users --",
"1; WAITFOR DELAY '0:0:5' --"
]
for payload in payloads:
response = requests.get(
f"{BASE_URL}/api/search",
params={"q": payload}
)
# Should return empty results or 400, never 500
assert response.status_code != 500, (
f"Potential SQL injection with payload: {payload}"
)
def test_authorization_cant_access_other_users():
"""Verify users can't access other users' data"""
# Login as user A
token_a = login("userA@test.com", "password")
# Try to access user B's data
response = requests.get(
f"{BASE_URL}/api/users/user-b-id",
headers={"Authorization": f"Bearer {token_a}"}
)
assert response.status_code == 403
def test_sensitive_data_not_in_response():
"""Verify passwords and tokens aren't exposed"""
response = requests.get(
f"{BASE_URL}/api/users/me",
headers={"Authorization": f"Bearer {valid_token}"}
)
data = response.json()
assert "password" not in data
assert "passwordHash" not in data
assert "apiSecret" not in data
When to use: Every API that handles user data, authentication, or financial transactions. Use OWASP API Security Top 10 as your checklist.
5. Performance Testing
Performance testing measures how the API behaves under load: response times, throughput, and error rates at various concurrency levels. A user registration endpoint that responds in 100ms with one request might take 5 seconds with 500 concurrent requests if the database connection pool is undersized.
Use tools like JMeter, k6, or Gatling to simulate realistic load patterns. Key metrics: P95 and P99 response times (not averages), throughput at target concurrency, error rate under load, and resource utilization on the server side.
When to use: Any API that will handle more than trivial traffic. Especially important before product launches, marketing campaigns, or seasonal traffic spikes.
6. Fuzz Testing
Fuzz testing sends malformed, unexpected, or random data to API endpoints to find crashes, memory leaks, and unhandled exceptions. Instead of carefully crafted test inputs, fuzz testing throws chaos at the API and watches for anything that breaks.
Examples of fuzz inputs: extremely long strings (100,000+ characters), Unicode edge cases (null bytes, RTL markers, emoji sequences), negative numbers where positives are expected, nested JSON structures thousands of levels deep, and binary data in text fields.
When to use: APIs that accept user input, file uploads, or complex data structures. Particularly valuable for finding crashes and security vulnerabilities that structured tests miss.
7. Idempotency Testing
Idempotency testing verifies that sending the same request multiple times produces the same result as sending it once. This matters enormously for payment processing, order placement, and any state-changing operation where network retries can occur.
def test_payment_idempotency():
"""Same payment request sent twice should not double-charge"""
idempotency_key = str(uuid.uuid4())
payment_data = {
"amount": 99.99,
"currency": "USD",
"customer_id": "cust_123"
}
headers = {
"Authorization": f"Bearer {token}",
"Idempotency-Key": idempotency_key
}
# First request
response1 = requests.post(
f"{BASE_URL}/api/payments",
json=payment_data,
headers=headers
)
assert response1.status_code == 201
payment_id_1 = response1.json()["id"]
# Same request with same idempotency key
response2 = requests.post(
f"{BASE_URL}/api/payments",
json=payment_data,
headers=headers
)
# Should return the same payment, not create a new one
assert response2.status_code in [200, 201]
payment_id_2 = response2.json()["id"]
assert payment_id_1 == payment_id_2 # Same payment returned
When to use: Any API that creates resources, processes payments, or changes state. Network failures cause retries. If your API isn’t idempotent, retries cause duplicates.
8. Validation Testing
Validation testing verifies that the API correctly rejects invalid inputs and returns helpful error messages. This goes beyond functional testing by systematically testing every validation rule: minimum and maximum lengths, required vs. optional fields, valid formats (email, phone, URL), numeric ranges, and enum constraints.
Good validation returns specific, actionable error messages: “Email must be a valid email address” instead of “Validation failed.” Great validation returns all errors at once instead of one at a time.
When to use: Every API that accepts input. Weak validation is the root cause of most data quality issues and many security vulnerabilities.
9. Error Handling Testing
Error handling testing goes beyond validation to test how the API behaves when things go wrong internally: database timeouts, external service failures, out-of-memory conditions, and unexpected data states. The goal is to verify that the API fails gracefully rather than exposing stack traces, leaking sensitive data, or leaving the system in an inconsistent state.
Test scenarios include: what happens when the database is unavailable? Does the API return 503 with a retry-after header, or does it return 500 with a stack trace? When an external payment provider times out, does the API roll back the order or leave it in a partial state?
When to use: Any API where graceful degradation matters — which is every API in production. Pay special attention to APIs that coordinate multiple services.
10. End-to-End API Chain Testing
End-to-end chain testing verifies complete business workflows that span multiple API calls. User registers → receives verification email → clicks verification link → logs in → creates an order → receives confirmation. Each API call depends on the result of the previous one.
def test_complete_user_journey():
"""Test the full user lifecycle through API chain"""
# Step 1: Register
reg_response = requests.post(f"{BASE_URL}/api/users", json={
"email": "journey@test.com",
"password": "SecurePass123!",
"name": "Journey Test"
})
assert reg_response.status_code == 201
user_id = reg_response.json()["id"]
# Step 2: Verify email (simulate)
verify_response = requests.post(
f"{BASE_URL}/api/users/{user_id}/verify",
json={"token": get_verification_token(user_id)}
)
assert verify_response.status_code == 200
# Step 3: Login
login_response = requests.post(f"{BASE_URL}/api/auth/login", json={
"email": "journey@test.com",
"password": "SecurePass123!"
})
assert login_response.status_code == 200
token = login_response.json()["accessToken"]
headers = {"Authorization": f"Bearer {token}"}
# Step 4: Create order
order_response = requests.post(
f"{BASE_URL}/api/orders",
json={"items": [{"productId": "prod_1", "quantity": 2}]},
headers=headers
)
assert order_response.status_code == 201
order_id = order_response.json()["id"]
# Step 5: Verify order status
status_response = requests.get(
f"{BASE_URL}/api/orders/{order_id}",
headers=headers
)
assert status_response.status_code == 200
assert status_response.json()["status"] == "pending"
assert status_response.json()["userId"] == user_id
When to use: For validating critical business workflows that span multiple endpoints. These are your highest-value tests because they catch integration issues between API calls that individual endpoint tests miss.
Building a Complete API Test Strategy
You don’t need to implement all ten types at once. Start with functional tests (type 1) and validation tests (type 8) as your baseline. Add security tests (type 4) and error handling tests (type 9) within the first month. Introduce contract tests (type 2), integration tests (type 3), and end-to-end chain tests (type 10) in month two. Add performance (type 5), fuzz (type 6), and idempotency (type 7) testing in month three.
Each layer catches different categories of bugs. Together, they provide comprehensive coverage that prevents the kind of Friday afternoon deployment that turns into a Monday morning incident.
Frequently Asked Questions
Which testing tools should I use?
RestAssured (Java), pytest + requests (Python), or Playwright’s API testing (TypeScript) for functional and integration tests. JMeter or k6 for performance. OWASP ZAP for security scanning. Pact for contract testing. The language matters less than the coverage.
How do I prioritize which API to test first?
Start with revenue-critical APIs (payment, checkout), then user-facing APIs (authentication, registration), then internal APIs. Within each API, prioritize by risk: endpoints that handle money, PII, or state changes get tested first.
Should API tests run in CI/CD?
Absolutely. Functional, validation, contract, and integration tests should run on every PR. Security and fuzz tests can run nightly. Performance tests can run weekly or before releases. End-to-end chain tests should run on every deployment to staging.
The Bottom Line
The developer who shipped the vulnerable registration endpoint didn’t skip testing. They just tested one dimension — functionality — and called it done. The 400 duplicate accounts, crashed email service, and SQL injection vulnerability lived in the nine dimensions they didn’t test.
API testing is ten things. Most teams do two. Close the gap, and you close the door on the production incidents that keep engineering teams up at night.
References
- RestAssured Documentation — Java API testing framework
- Pact Documentation — Consumer-driven contract testing
- OWASP API Security Top 10 — API security vulnerability checklist
- Apache JMeter — API performance testing
- k6 Documentation — Modern load testing for APIs
- Playwright API Testing — TypeScript API testing framework
- OpenAPI Specification — API documentation and contract standard
- Postman API Testing — API development and testing platform
- pytest Documentation — Python test framework
- Ministry of Testing — API testing community resources
