Intermediate
Chapter 11 · 10 min read
Error Handling & Negative Testing
Test error scenarios — 400, 401, 403, 404, 500 responses. Validate error messages, handle timeouts, and test boundary conditions.
Negative & Error Testing
Happy-path tests prove the API works when used correctly. Negative tests prove it fails gracefully when used incorrectly. Both are equally important.
Common Error Scenarios
- 400 Bad Request — Invalid request body, missing required fields, wrong data types
- 401 Unauthorized — Missing or invalid authentication
- 403 Forbidden — Valid auth but insufficient permissions
- 404 Not Found — Resource doesn't exist
- 405 Method Not Allowed — Using wrong HTTP method
- 422 Unprocessable Entity — Valid JSON but invalid data (e.g., negative age)
- 429 Too Many Requests — Rate limit exceeded
- 500 Internal Server Error — Server-side failures
Boundary Testing
Test with empty strings, very long strings, special characters, zero, negative numbers, null values, and maximum values. These edge cases often reveal bugs.
negative-testing.test.js
// 404 — Resource not found
const notFound = await fetch('https://jsonplaceholder.typicode.com/posts/99999');
console.assert(notFound.status === 404, 'Should return 404');
// Invalid endpoint
const invalid = await fetch('https://jsonplaceholder.typicode.com/nonexistent');
console.assert(invalid.status === 404, 'Invalid endpoint should return 404');
// Bad request body — missing required fields
const badCreate = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}) // Empty body
});
// Note: JSONPlaceholder is lenient, real APIs would return 400
// Timeout handling
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
try {
const resp = await fetch('https://jsonplaceholder.typicode.com/posts', {
signal: controller.signal
});
clearTimeout(timeoutId);
console.log('Request completed within timeout');
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request timed out after 3 seconds');
}
}
// Boundary testing — special characters
const specialChars = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: '<script>alert("xss")</script>',
body: "Test with special chars: !@#$%^&*()'\"",
userId: 1
})
});
console.assert(specialChars.ok, 'Should handle special characters');
// Empty string fields
const emptyFields = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: '', body: '', userId: 1 })
});
console.log('All negative tests passed!');
NegativeTestingTest.java
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import io.restassured.RestAssured;
import io.restassured.config.HttpClientConfig;
import org.testng.annotations.Test;
public class NegativeTestingTest {
private static final String BASE = "https://jsonplaceholder.typicode.com";
@Test
public void testNotFound_404() {
given().baseUri(BASE)
.when().get("/posts/99999")
.then()
.statusCode(404);
}
@Test
public void testInvalidEndpoint_404() {
given().baseUri(BASE)
.when().get("/nonexistent")
.then()
.statusCode(404);
}
@Test
public void testEmptyBody() {
given().baseUri(BASE)
.contentType("application/json")
.body("{}")
.when()
.post("/posts")
.then()
.statusCode(anyOf(equalTo(201), equalTo(400)));
}
@Test
public void testSpecialCharacters() {
String body = "{"
+ "\"title\": \"<script>alert(xss)</script>\","
+ "\"body\": \"Special: !@#$%^&*()\","
+ "\"userId\": 1"
+ "}";
given().baseUri(BASE)
.contentType("application/json")
.body(body)
.when()
.post("/posts")
.then()
.statusCode(201);
}
@Test
public void testWithTimeout() {
given()
.baseUri(BASE)
.config(RestAssured.config()
.httpClient(HttpClientConfig.httpClientConfig()
.setParam("http.connection.timeout", 3000)
.setParam("http.socket.timeout", 3000)))
.when()
.get("/posts")
.then()
.statusCode(200)
.time(lessThan(5000L));
}
@Test
public void testInvalidJsonBody() {
given().baseUri(BASE)
.contentType("application/json")
.body("{ invalid json }")
.when()
.post("/posts")
.then()
.statusCode(anyOf(equalTo(400), equalTo(500)));
}
}
test_negative_testing.py
import requests
import pytest
BASE_URL = 'https://jsonplaceholder.typicode.com'
def test_not_found_404():
"""Resource not found"""
response = requests.get(f'{BASE_URL}/posts/99999')
assert response.status_code == 404
def test_invalid_endpoint():
"""Invalid endpoint"""
response = requests.get(f'{BASE_URL}/nonexistent')
assert response.status_code == 404
def test_empty_body():
"""POST with empty body"""
response = requests.post(
f'{BASE_URL}/posts',
json={},
headers={'Content-Type': 'application/json'}
)
# JSONPlaceholder is lenient; real APIs would return 400
assert response.status_code in [201, 400]
def test_special_characters():
"""Test with special characters (XSS attempt)"""
response = requests.post(
f'{BASE_URL}/posts',
json={
'title': '<script>alert("xss")</script>',
'body': "Special: !@#$%^&*()'\"",
'userId': 1
}
)
assert response.ok
def test_timeout_handling():
"""Handle request timeouts"""
try:
response = requests.get(f'{BASE_URL}/posts', timeout=3)
assert response.ok
except requests.Timeout:
print('Request timed out as expected')
def test_connection_error():
"""Handle connection errors gracefully"""
with pytest.raises(requests.ConnectionError):
requests.get('http://nonexistent-domain-12345.com', timeout=2)
def test_boundary_empty_strings():
"""Empty string fields"""
response = requests.post(
f'{BASE_URL}/posts',
json={'title': '', 'body': '', 'userId': 1}
)
assert response.status_code in [201, 400, 422]
API Testing
Intermediate
Error Handling & Negative Testing
Written by PV
© 2026 All Rights Reserved