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