← Back to Learning Modules

API Test Automation

Master the art of automated API testing with REST and GraphQL

Overview

API (Application Programming Interface) testing is crucial for ensuring that data exchange between different software components works correctly. This module covers comprehensive API testing strategies, from basic HTTP requests to complex authentication scenarios.

What you'll learn: REST API testing, GraphQL testing, authentication methods, data validation, error handling, and performance testing for APIs.

REST API Testing

REST APIs are the backbone of modern web applications. Here's how to test them effectively:

Basic GET Request Testing

// Using Fetch API with Jest const request = require('supertest'); const app = require('../app'); describe('GET /api/users', () => { test('should return list of users', async () => { const response = await request(app) .get('/api/users') .expect(200); expect(response.body).toHaveProperty('users'); expect(Array.isArray(response.body.users)).toBe(true); expect(response.body.users.length).toBeGreaterThan(0); // Validate first user structure const firstUser = response.body.users[0]; expect(firstUser).toHaveProperty('id'); expect(firstUser).toHaveProperty('name'); expect(firstUser).toHaveProperty('email'); }); test('should return user by ID', async () => { const userId = 1; const response = await request(app) .get(`/api/users/${userId}`) .expect(200); expect(response.body).toHaveProperty('id', userId); expect(response.body).toHaveProperty('name'); expect(response.body).toHaveProperty('email'); // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; expect(response.body.email).toMatch(emailRegex); }); test('should return 404 for non-existent user', async () => { const nonExistentId = 99999; const response = await request(app) .get(`/api/users/${nonExistentId}`) .expect(404); expect(response.body).toHaveProperty('error'); }); test('should validate response time', async () => { const startTime = Date.now(); await request(app) .get('/api/users') .expect(200); const responseTime = Date.now() - startTime; expect(responseTime).toBeLessThan(1000); // Less than 1 second }); });
// Using REST Assured with TestNG import static io.restassured.RestAssured.*; import static org.hamcrest.Matchers.*; import org.testng.annotations.Test; import io.restassured.response.Response; public class UserApiTest { private static final String BASE_URL = "https://api.example.com"; @Test public void testGetAllUsers() { given() .baseUri(BASE_URL) .header("Content-Type", "application/json") .when() .get("/api/users") .then() .statusCode(200) .body("users", hasSize(greaterThan(0))) .body("users[0]", hasKey("id")) .body("users[0]", hasKey("name")) .body("users[0]", hasKey("email")) .time(lessThan(1000L)); // Response time validation } @Test public void testGetUserById() { int userId = 1; given() .baseUri(BASE_URL) .pathParam("id", userId) .when() .get("/api/users/{id}") .then() .statusCode(200) .body("id", equalTo(userId)) .body("name", not(emptyString())) .body("email", matchesPattern(".*@.*\\..*")); } @Test public void testGetUserNotFound() { int nonExistentId = 99999; given() .baseUri(BASE_URL) .pathParam("id", nonExistentId) .when() .get("/api/users/{id}") .then() .statusCode(404) .body("error", not(emptyString())); } @Test public void testGetUsersWithPagination() { given() .baseUri(BASE_URL) .queryParam("page", 1) .queryParam("limit", 10) .when() .get("/api/users") .then() .statusCode(200) .body("users", hasSize(lessThanOrEqualTo(10))) .body("pagination.page", equalTo(1)) .body("pagination.limit", equalTo(10)); } }
# Using Requests with pytest import requests import pytest import time import re class TestUserAPI: BASE_URL = "https://api.example.com" def test_get_all_users(self): response = requests.get(f"{self.BASE_URL}/api/users") assert response.status_code == 200 data = response.json() assert "users" in data assert isinstance(data["users"], list) assert len(data["users"]) > 0 # Validate user structure user = data["users"][0] assert "id" in user assert "name" in user assert "email" in user # Validate data types assert isinstance(user["id"], int) assert isinstance(user["name"], str) assert isinstance(user["email"], str) def test_get_user_by_id(self): user_id = 1 response = requests.get(f"{self.BASE_URL}/api/users/{user_id}") assert response.status_code == 200 user = response.json() assert user["id"] == user_id assert len(user["name"]) > 0 assert "@" in user["email"] # Validate email format with regex email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' assert re.match(email_pattern, user["email"]) def test_get_user_not_found(self): non_existent_id = 99999 response = requests.get(f"{self.BASE_URL}/api/users/{non_existent_id}") assert response.status_code == 404 error_data = response.json() assert "error" in error_data def test_response_time(self): start_time = time.time() response = requests.get(f"{self.BASE_URL}/api/users") response_time = time.time() - start_time assert response.status_code == 200 assert response_time < 1.0 # Less than 1 second def test_get_users_with_pagination(self): params = {"page": 1, "limit": 10} response = requests.get( f"{self.BASE_URL}/api/users", params=params ) assert response.status_code == 200 data = response.json() assert len(data["users"]) <= 10 assert data["pagination"]["page"] == 1 assert data["pagination"]["limit"] == 10

POST Request Testing with Data Validation

// Testing POST requests with validation describe('POST /api/users', () => { test('should create new user successfully', async () => { const newUser = { name: 'John Doe', email: 'john.doe@example.com', age: 30, role: 'user' }; const response = await request(app) .post('/api/users') .send(newUser) .expect(201); expect(response.body).toHaveProperty('id'); expect(response.body.name).toBe(newUser.name); expect(response.body.email).toBe(newUser.email); expect(response.body.age).toBe(newUser.age); expect(response.body.role).toBe(newUser.role); }); test('should return 400 for invalid email', async () => { const invalidUser = { name: 'John Doe', email: 'invalid-email', age: 30 }; const response = await request(app) .post('/api/users') .send(invalidUser) .expect(400); expect(response.body).toHaveProperty('error'); expect(response.body.error).toContain('email'); }); test('should return 400 for missing required fields', async () => { const incompleteUser = { name: 'John Doe' // Missing email and other required fields }; const response = await request(app) .post('/api/users') .send(incompleteUser) .expect(400); expect(response.body).toHaveProperty('error'); expect(response.body.error).toContain('required'); }); test('should return 409 for duplicate email', async () => { const duplicateUser = { name: 'Jane Doe', email: 'existing@example.com', // Assume this email already exists age: 25 }; const response = await request(app) .post('/api/users') .send(duplicateUser) .expect(409); expect(response.body).toHaveProperty('error'); expect(response.body.error).toContain('already exists'); }); test('should validate age constraints', async () => { const invalidAgeUser = { name: 'Too Young', email: 'young@example.com', age: -5 // Invalid age }; const response = await request(app) .post('/api/users') .send(invalidAgeUser) .expect(400); expect(response.body).toHaveProperty('error'); expect(response.body.error).toContain('age'); }); });
@Test public void testCreateUserSuccess() { String requestBody = """ { "name": "John Doe", "email": "john.doe@example.com", "age": 30, "role": "user" } """; given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(requestBody) .when() .post("/api/users") .then() .statusCode(201) .body("id", notNullValue()) .body("name", equalTo("John Doe")) .body("email", equalTo("john.doe@example.com")) .body("age", equalTo(30)) .body("role", equalTo("user")); } @Test public void testCreateUserInvalidEmail() { String requestBody = """ { "name": "John Doe", "email": "invalid-email", "age": 30 } """; given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(requestBody) .when() .post("/api/users") .then() .statusCode(400) .body("error", containsString("email")); } @Test public void testCreateUserMissingFields() { String requestBody = """ { "name": "John Doe" } """; given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(requestBody) .when() .post("/api/users") .then() .statusCode(400) .body("error", containsString("required")); } @Test public void testCreateUserDuplicateEmail() { String requestBody = """ { "name": "Jane Doe", "email": "existing@example.com", "age": 25 } """; given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(requestBody) .when() .post("/api/users") .then() .statusCode(409) .body("error", containsString("already exists")); } @Test public void testCreateUserInvalidAge() { String requestBody = """ { "name": "Too Young", "email": "young@example.com", "age": -5 } """; given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(requestBody) .when() .post("/api/users") .then() .statusCode(400) .body("error", containsString("age")); } @Test public void testCreateUserResponseStructure() { String requestBody = """ { "name": "Test User", "email": "test.user@example.com", "age": 28 } """; Response response = given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(requestBody) .when() .post("/api/users") .then() .statusCode(201) .extract().response(); // Validate response structure and content assertThat(response.jsonPath().getInt("id")).isGreaterThan(0); assertThat(response.jsonPath().getString("createdAt")).isNotEmpty(); assertThat(response.jsonPath().getString("updatedAt")).isNotEmpty(); }
def test_create_user_success(self): user_data = { "name": "John Doe", "email": "john.doe@example.com", "age": 30, "role": "user" } response = requests.post( f"{self.BASE_URL}/api/users", json=user_data ) assert response.status_code == 201 created_user = response.json() assert "id" in created_user assert created_user["name"] == user_data["name"] assert created_user["email"] == user_data["email"] assert created_user["age"] == user_data["age"] assert created_user["role"] == user_data["role"] # Validate response structure assert "createdAt" in created_user assert "updatedAt" in created_user def test_create_user_invalid_email(self): invalid_user = { "name": "John Doe", "email": "invalid-email", "age": 30 } response = requests.post( f"{self.BASE_URL}/api/users", json=invalid_user ) assert response.status_code == 400 error_response = response.json() assert "error" in error_response assert "email" in error_response["error"].lower() def test_create_user_missing_fields(self): incomplete_user = { "name": "John Doe" # Missing email and other required fields } response = requests.post( f"{self.BASE_URL}/api/users", json=incomplete_user ) assert response.status_code == 400 error_response = response.json() assert "error" in error_response assert "required" in error_response["error"].lower() def test_create_user_duplicate_email(self): duplicate_user = { "name": "Jane Doe", "email": "existing@example.com", # Assume this email already exists "age": 25 } response = requests.post( f"{self.BASE_URL}/api/users", json=duplicate_user ) assert response.status_code == 409 error_response = response.json() assert "error" in error_response assert "already exists" in error_response["error"].lower() def test_create_user_invalid_age(self): invalid_age_user = { "name": "Too Young", "email": "young@example.com", "age": -5 # Invalid age } response = requests.post( f"{self.BASE_URL}/api/users", json=invalid_age_user ) assert response.status_code == 400 error_response = response.json() assert "error" in error_response assert "age" in error_response["error"].lower() def test_create_user_data_types(self): """Test that API validates data types correctly""" invalid_data_user = { "name": 12345, # Should be string "email": "test@example.com", "age": "thirty" # Should be number } response = requests.post( f"{self.BASE_URL}/api/users", json=invalid_data_user ) assert response.status_code == 400 error_response = response.json() assert "error" in error_response

Authentication Testing

Most APIs require authentication. Here's how to test different authentication methods:

JWT Token Authentication

// JWT Authentication Testing describe('Authentication Tests', () => { let authToken; beforeAll(async () => { // Login to get authentication token const loginResponse = await request(app) .post('/api/auth/login') .send({ email: 'test@example.com', password: 'password123' }) .expect(200); authToken = loginResponse.body.token; // Validate token structure expect(authToken).toBeDefined(); expect(typeof authToken).toBe('string'); expect(authToken.split('.')).toHaveLength(3); // JWT has 3 parts }); test('should access protected endpoint with valid token', async () => { const response = await request(app) .get('/api/protected/profile') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(response.body).toHaveProperty('user'); expect(response.body.user).toHaveProperty('id'); expect(response.body.user).toHaveProperty('email'); }); test('should reject request without token', async () => { const response = await request(app) .get('/api/protected/profile') .expect(401); expect(response.body).toHaveProperty('error'); expect(response.body.error).toContain('token'); }); test('should reject request with invalid token', async () => { const response = await request(app) .get('/api/protected/profile') .set('Authorization', 'Bearer invalid-token') .expect(401); expect(response.body).toHaveProperty('error'); }); test('should reject request with malformed header', async () => { const response = await request(app) .get('/api/protected/profile') .set('Authorization', authToken) // Missing "Bearer " prefix .expect(401); expect(response.body).toHaveProperty('error'); }); test('should handle token expiration', async () => { // Mock an expired token or wait for real expiration const expiredToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.expired.token'; const response = await request(app) .get('/api/protected/profile') .set('Authorization', `Bearer ${expiredToken}`) .expect(401); expect(response.body.error).toContain('expired'); }); test('should refresh expired token', async () => { // Assume we have a refresh token const refreshResponse = await request(app) .post('/api/auth/refresh') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(refreshResponse.body).toHaveProperty('token'); expect(refreshResponse.body.token).not.toBe(authToken); }); });
public class AuthenticationTest { private String authToken; @BeforeClass public void getAuthToken() { String loginBody = """ { "email": "test@example.com", "password": "password123" } """; Response response = given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(loginBody) .when() .post("/api/auth/login") .then() .statusCode(200) .body("token", notNullValue()) .extract().response(); authToken = response.jsonPath().getString("token"); // Validate JWT structure assertThat(authToken.split("\\.")).hasSize(3); } @Test public void testProtectedEndpointWithValidToken() { given() .baseUri(BASE_URL) .header("Authorization", "Bearer " + authToken) .when() .get("/api/protected/profile") .then() .statusCode(200) .body("user", notNullValue()) .body("user.id", notNullValue()) .body("user.email", notNullValue()); } @Test public void testProtectedEndpointWithoutToken() { given() .baseUri(BASE_URL) .when() .get("/api/protected/profile") .then() .statusCode(401) .body("error", containsString("token")); } @Test public void testProtectedEndpointWithInvalidToken() { given() .baseUri(BASE_URL) .header("Authorization", "Bearer invalid-token") .when() .get("/api/protected/profile") .then() .statusCode(401) .body("error", notNullValue()); } @Test public void testProtectedEndpointWithMalformedHeader() { given() .baseUri(BASE_URL) .header("Authorization", authToken) // Missing "Bearer " prefix .when() .get("/api/protected/profile") .then() .statusCode(401) .body("error", notNullValue()); } @Test public void testTokenExpiration() { String expiredToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.expired.token"; given() .baseUri(BASE_URL) .header("Authorization", "Bearer " + expiredToken) .when() .get("/api/protected/profile") .then() .statusCode(401) .body("error", containsString("expired")); } @Test public void testTokenRefresh() { given() .baseUri(BASE_URL) .header("Authorization", "Bearer " + authToken) .when() .post("/api/auth/refresh") .then() .statusCode(200) .body("token", notNullValue()) .body("token", not(equalTo(authToken))); } @Test public void testRoleBasedAccess() { // Test admin-only endpoint given() .baseUri(BASE_URL) .header("Authorization", "Bearer " + authToken) .when() .get("/api/admin/users") .then() .statusCode(anyOf(is(200), is(403))); // Depends on user role } }
class TestAuthentication: def setup_class(self): # Get authentication token login_data = { "email": "test@example.com", "password": "password123" } response = requests.post( f"{self.BASE_URL}/api/auth/login", json=login_data ) assert response.status_code == 200 response_data = response.json() self.auth_token = response_data["token"] # Validate JWT structure assert len(self.auth_token.split('.')) == 3 def test_protected_endpoint_with_valid_token(self): headers = {"Authorization": f"Bearer {self.auth_token}"} response = requests.get( f"{self.BASE_URL}/api/protected/profile", headers=headers ) assert response.status_code == 200 data = response.json() assert "user" in data assert "id" in data["user"] assert "email" in data["user"] def test_protected_endpoint_without_token(self): response = requests.get(f"{self.BASE_URL}/api/protected/profile") assert response.status_code == 401 error_data = response.json() assert "error" in error_data assert "token" in error_data["error"].lower() def test_protected_endpoint_with_invalid_token(self): headers = {"Authorization": "Bearer invalid-token"} response = requests.get( f"{self.BASE_URL}/api/protected/profile", headers=headers ) assert response.status_code == 401 error_data = response.json() assert "error" in error_data def test_protected_endpoint_with_malformed_header(self): headers = {"Authorization": self.auth_token} # Missing "Bearer " prefix response = requests.get( f"{self.BASE_URL}/api/protected/profile", headers=headers ) assert response.status_code == 401 error_data = response.json() assert "error" in error_data def test_token_expiration(self): # Mock an expired token expired_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.expired.token" headers = {"Authorization": f"Bearer {expired_token}"} response = requests.get( f"{self.BASE_URL}/api/protected/profile", headers=headers ) assert response.status_code == 401 error_data = response.json() assert "expired" in error_data["error"].lower() def test_token_refresh(self): headers = {"Authorization": f"Bearer {self.auth_token}"} response = requests.post( f"{self.BASE_URL}/api/auth/refresh", headers=headers ) assert response.status_code == 200 refresh_data = response.json() assert "token" in refresh_data assert refresh_data["token"] != self.auth_token def test_role_based_access(self): """Test role-based access control""" headers = {"Authorization": f"Bearer {self.auth_token}"} response = requests.get( f"{self.BASE_URL}/api/admin/users", headers=headers ) # Should be either 200 (if user is admin) or 403 (if not admin) assert response.status_code in [200, 403] if response.status_code == 403: error_data = response.json() assert "error" in error_data assert "permission" in error_data["error"].lower() \ or "forbidden" in error_data["error"].lower() def test_concurrent_requests_with_same_token(self): """Test that the same token can be used concurrently""" import threading headers = {"Authorization": f"Bearer {self.auth_token}"} results = [] def make_request(): response = requests.get( f"{self.BASE_URL}/api/protected/profile", headers=headers ) results.append(response.status_code) # Make 5 concurrent requests threads = [] for _ in range(5): thread = threading.Thread(target=make_request) threads.append(thread) thread.start() for thread in threads: thread.join() # All requests should succeed assert all(status == 200 for status in results)

GraphQL Testing

GraphQL APIs require different testing approaches due to their flexible query nature:

// GraphQL Testing with Jest describe('GraphQL API Tests', () => { test('should query user data successfully', async () => { const query = ` query GetUser($id: ID!) { user(id: $id) { id name email posts { id title createdAt } } } `; const variables = { id: '1' }; const response = await request(app) .post('/graphql') .send({ query, variables }) .expect(200); expect(response.body.data).toHaveProperty('user'); expect(response.body.data.user.id).toBe('1'); expect(response.body.data.user.posts).toBeDefined(); expect(Array.isArray(response.body.data.user.posts)).toBe(true); }); test('should handle GraphQL mutations', async () => { const mutation = ` mutation CreatePost($input: PostInput!) { createPost(input: $input) { id title content author { id name } createdAt } } `; const variables = { input: { title: 'Test Post', content: 'This is a test post content', authorId: '1' } }; const response = await request(app) .post('/graphql') .send({ query: mutation, variables }) .expect(200); expect(response.body.data.createPost).toHaveProperty('id'); expect(response.body.data.createPost.title).toBe(variables.input.title); expect(response.body.data.createPost.author.id).toBe(variables.input.authorId); }); test('should handle GraphQL errors gracefully', async () => { const invalidQuery = ` query GetUser($id: ID!) { user(id: $id) { id nonExistentField } } `; const variables = { id: '1' }; const response = await request(app) .post('/graphql') .send({ query: invalidQuery, variables }) .expect(400); expect(response.body).toHaveProperty('errors'); expect(Array.isArray(response.body.errors)).toBe(true); expect(response.body.errors[0]).toHaveProperty('message'); }); test('should support nested queries', async () => { const nestedQuery = ` query GetUserWithPostsAndComments($userId: ID!) { user(id: $userId) { id name posts { id title comments { id content author { id name } } } } } `; const variables = { userId: '1' }; const response = await request(app) .post('/graphql') .send({ query: nestedQuery, variables }) .expect(200); const user = response.body.data.user; expect(user).toHaveProperty('posts'); if (user.posts.length > 0) { const firstPost = user.posts[0]; expect(firstPost).toHaveProperty('comments'); if (firstPost.comments.length > 0) { const firstComment = firstPost.comments[0]; expect(firstComment).toHaveProperty('author'); expect(firstComment.author).toHaveProperty('name'); } } }); test('should validate input data in mutations', async () => { const invalidMutation = ` mutation CreatePost($input: PostInput!) { createPost(input: $input) { id title } } `; const invalidVariables = { input: { title: "", // Empty title should be invalid content: "Content", authorId: "999" // Non-existent author } }; const response = await request(app) .post('/graphql') .send({ query: invalidMutation, variables: invalidVariables }) .expect(200); // GraphQL returns 200 but with errors expect(response.body).toHaveProperty('errors'); expect(response.body.errors.length).toBeGreaterThan(0); }); });
@Test public void testGraphQLUserQuery() { String query = """ { "query": "query GetUser($id: ID!) { user(id: $id) { id name email posts { id title createdAt } } }", "variables": { "id": "1" } } """; given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(query) .when() .post("/graphql") .then() .statusCode(200) .body("data.user.id", equalTo("1")) .body("data.user.name", notNullValue()) .body("data.user.posts", notNullValue()) .body("data.user.posts", hasSize(greaterThanOrEqualTo(0))); } @Test public void testGraphQLMutation() { String mutation = """ { "query": "mutation CreatePost($input: PostInput!) { createPost(input: $input) { id title content author { id name } createdAt } }", "variables": { "input": { "title": "Test Post", "content": "This is a test post content", "authorId": "1" } } } """; given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(mutation) .when() .post("/graphql") .then() .statusCode(200) .body("data.createPost.id", notNullValue()) .body("data.createPost.title", equalTo("Test Post")) .body("data.createPost.author.id", equalTo("1")) .body("data.createPost.createdAt", notNullValue()); } @Test public void testGraphQLErrorHandling() { String invalidQuery = """ { "query": "query GetUser($id: ID!) { user(id: $id) { id nonExistentField } }", "variables": { "id": "1" } } """; given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(invalidQuery) .when() .post("/graphql") .then() .statusCode(400) .body("errors", hasSize(greaterThan(0))) .body("errors[0].message", notNullValue()); } @Test public void testNestedGraphQLQuery() { String nestedQuery = """ { "query": "query GetUserWithPostsAndComments($userId: ID!) { user(id: $userId) { id name posts { id title comments { id content author { id name } } } } }", "variables": { "userId": "1" } } """; Response response = given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(nestedQuery) .when() .post("/graphql") .then() .statusCode(200) .body("data.user", notNullValue()) .body("data.user.posts", notNullValue()) .extract().response(); // Additional validation for nested structures JsonPath jsonPath = response.jsonPath(); List<Object> posts = jsonPath.getList("data.user.posts"); if (!posts.isEmpty()) { assertThat(jsonPath.get("data.user.posts[0].comments")).isNotNull(); } } @Test public void testGraphQLInputValidation() { String invalidMutation = """ { "query": "mutation CreatePost($input: PostInput!) { createPost(input: $input) { id title } }", "variables": { "input": { "title": "", "content": "Content", "authorId": "999" } } } """; given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(invalidMutation) .when() .post("/graphql") .then() .statusCode(200) // GraphQL returns 200 but with errors .body("errors", hasSize(greaterThan(0))) .body("errors[0].message", containsString("validation")); } @Test public void testGraphQLFieldSelection() { String selectiveQuery = """ { "query": "query GetUserSelective($id: ID!) { user(id: $id) { name email } }", "variables": { "id": "1" } } """; Response response = given() .baseUri(BASE_URL) .contentType(ContentType.JSON) .body(selectiveQuery) .when() .post("/graphql") .then() .