API Testing Guide: What to Test and How to Structure Your API Test Suite
API testing is high-ROI automation that most teams under-invest in. It's faster and more reliable than UI testing and covers the business logic where most bugs live. Here's how to build a thorough API test suite.
On this page
If you could only automate one layer of your application, automate the API layer. It's faster than UI automation, more stable, and tests exactly where your business logic lives.
Here's everything you need to build a complete API test suite.
What API Tests Verify
A thorough API test suite covers:
- Happy path — expected inputs produce expected outputs
- Validation — invalid inputs are rejected with appropriate error codes
- Authorization — unauthorized requests are rejected, authorized requests succeed
- Edge cases — boundary values, empty collections, maximum sizes
- Error handling — server errors return meaningful responses, not stack traces
- Contract — the response schema matches what clients expect
The Test Structure: Given-When-Then for APIs
Every API test follows a clear structure:
@Test
fun `creating a task returns 201 with the created task`() {
// Given
val request = CreateTaskRequest(
title = "Buy groceries",
dueDate = "2026-04-01"
)
// When
val response = api.post("/tasks", request)
// Then
assertEquals(201, response.statusCode)
val task: Task = response.body()
assertNotNull(task.id)
assertEquals("Buy groceries", task.title)
assertEquals("ACTIVE", task.status)
assertEquals("2026-04-01", task.dueDate)
}Testing HTTP Status Codes
Every endpoint should return the semantically correct status code. Test the code, not just that something came back:
| Scenario | Expected Status |
|---|---|
| Successful creation | 201 Created |
| Successful retrieval | 200 OK |
| Resource not found | 404 Not Found |
| Invalid input | 400 Bad Request |
| Unauthorized | 401 Unauthorized |
| Forbidden (authenticated but not allowed) | 403 Forbidden |
| Server error | 500 (and verify it's handled, not exposed) |
@Test
fun `getting nonexistent task returns 404`() {
val response = api.get("/tasks/nonexistent-id-12345")
assertEquals(404, response.statusCode)
}
@Test
fun `creating task without title returns 400`() {
val response = api.post("/tasks", mapOf("dueDate" to "2026-04-01"))
assertEquals(400, response.statusCode)
val error: ErrorResponse = response.body()
assertEquals("title is required", error.message)
}Testing Authorization
Authorization testing is often skipped. Don't skip it.
@Test
fun `unauthenticated request returns 401`() {
val response = api.get("/tasks") // No auth header
assertEquals(401, response.statusCode)
}
@Test
fun `user cannot access another user's tasks`() {
val userAToken = authenticate("user-a@example.com")
val userBToken = authenticate("user-b@example.com")
// User A creates a task
val task = api.withToken(userAToken).post("/tasks", CreateTaskRequest("Private task"))
assertEquals(201, task.statusCode)
val taskId = task.body<Task>().id
// User B tries to access User A's task
val response = api.withToken(userBToken).get("/tasks/$taskId")
assertEquals(403, response.statusCode) // or 404 if you don't want to leak existence
}Contract Testing
Contract testing verifies that the API response shape matches what consumers expect. This is critical when you have mobile apps that parse the response.
If the backend changes a field name from
taskIdid@Test
fun `task response includes expected fields`() {
val response = api.get("/tasks/${existingTask.id}")
val body = response.body<JsonObject>()
assertTrue(body.containsKey("id"))
assertTrue(body.containsKey("title"))
assertTrue(body.containsKey("status"))
assertTrue(body.containsKey("createdAt"))
assertFalse(body.containsKey("internalDatabaseId")) // Should not be exposed
}For more sophisticated contract testing between services, look at Pact.
Testing Pagination
@Test
fun `task list returns paginated results`() {
// Setup: create 25 tasks
repeat(25) { i -> api.post("/tasks", CreateTaskRequest("Task $i")) }
// First page
val page1 = api.get("/tasks?page=1&size=10")
val body1 = page1.body<PaginatedResponse<Task>>()
assertEquals(10, body1.items.size)
assertEquals(25, body1.total)
assertTrue(body1.hasNextPage)
// Second page
val page2 = api.get("/tasks?page=2&size=10")
val body2 = page2.body<PaginatedResponse<Task>>()
assertEquals(10, body2.items.size)
// Third page (partial)
val page3 = api.get("/tasks?page=3&size=10")
val body3 = page3.body<PaginatedResponse<Task>>()
assertEquals(5, body3.items.size)
assertFalse(body3.hasNextPage)
}Testing Search and Filtering
@Test
fun `filtering by status returns only matching tasks`() {
api.post("/tasks", CreateTaskRequest("Active task 1"))
api.post("/tasks", CreateTaskRequest("Active task 2"))
val completedTask = api.post("/tasks", CreateTaskRequest("Done task")).body<Task>()
api.patch("/tasks/${completedTask.id}", mapOf("status" to "COMPLETED"))
val response = api.get("/tasks?status=ACTIVE")
val tasks = response.body<List<Task>>()
assertEquals(2, tasks.size)
assertTrue(tasks.all { it.status == "ACTIVE" })
}Tooling
Kotlin/Android projects: Ktor Client or OkHttp for HTTP, JUnit 5 for test runner
Backend (JVM): REST-assured (Java/Kotlin), excellent DSL for API tests
Python: pytest + requests
JavaScript/TypeScript: Supertest (Node), Axios + Jest
Language-agnostic: Postman + Newman (run collections in CI), k6 (also does functional tests)
API Test Organization
Structure tests by resource and scenario:
tests/
api/
tasks/
TaskCreationTest.kt
TaskRetrievalTest.kt
TaskUpdateTest.kt
TaskDeletionTest.kt
auth/
AuthenticationTest.kt
AuthorizationTest.kt
users/
UserProfileTest.ktTakeaways
- API tests are the highest-ROI automation: fast, stable, test where bugs actually live
- Test status codes explicitly — not just "did something come back"
- Authorization testing is skipped most often and exploited most often
- Contract tests prevent breaking changes from reaching mobile clients
- Organize by resource and scenario — makes it obvious when coverage is missing
Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
