API Testing
What it is
Section titled “What it is”API testing exercises the service through its interface rather than its UI. You send requests (or invoke RPCs) and assert on status codes, headers, payload shape, business rules and error contracts. Because an API is the contract every client depends on, it is the highest leverage place to test behaviour.
client ──request──▶ API ──▶ service ──▶ datastore ◀─response── (status, body, headers, errors)Three interface styles dominate, and each needs a different testing stance:
- REST — resource URLs, HTTP verbs, JSON. Validate status codes, media types and schema.
- GraphQL — one endpoint, typed schema, arbitrary queries. Validate resolvers, nullability, error paths and query depth limits.
- gRPC — protobuf contracts, HTTP/2 streams. Validate message schemas, status codes and deadline/cancellation behaviour.
When to use it / when to avoid it
Section titled “When to use it / when to avoid it”Use it for
- Verifying the request/response contract every client relies on
- Business rules the UI hides or cannot reach (pagination edges, auth scopes, rate limits)
- Fast, stable regression coverage that runs without a browser
- Validating error handling: 4xx messages, 5xx behaviour, validation failures
Avoid it when
- You are re-asserting the same rule already covered by a faster unit or integration test
- You need to verify rendering, accessibility or interaction — that is end-to-end
- You want to test internal implementation detail rather than the published interface
Frameworks & tools
Section titled “Frameworks & tools”| Tool | Ecosystem | Notes |
|---|---|---|
| Postman / Newman | Any | GUI authoring plus CLI runner for CI |
| REST Assured | Java | Fluent DSL over REST endpoints, JUnit-friendly |
| Karate | Java / JVM | BDD-style API tests, built-in mocking and Gatling perf |
| Schemathesis | Python | Property-based, generates cases from OpenAPI/Swagger |
| Bruno | Any | Git-friendly, offline, plain-text collections |
| Robot Framework + RequestsLibrary | Python | Keyword-driven REST testing, easy to read and extend |
| Playwright APIRequestContext | Multi | Reuse browser fixtures for pure API checks |
| grpcurl / ghz | Go | gRPC reflection calls and load testing |
| Dredd | Node | Validate implementation against API Blueprint/OpenAPI |
Quick setup
Section titled “Quick setup”npm install -D vitest supertestnpm install -g newmannewman run collection.jsonpip install requests schemathesis robotframework robotframework-requestsschemathesis run --checks all https://api.example.com/openapi.jsongo install github.com/fullstorydev/grpcurl/cmd/grpcurl@latestgo get github.com/stretchr/testify<dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <version>5.5.0</version> <scope>test</scope></dependency>dotnet add package RestSharpdotnet add package xunitExample
Section titled “Example”import request from 'supertest';import { app } from '../src/app';
it('rejects a user without a name', async () => { const res = await request(app).post('/users').send({ name: '' }); expect(res.status).toBe(422); expect(res.body.errors[0].field).toBe('name');});import requests
def test_create_and_read(base_url): created = requests.post(f"{base_url}/users", json={"name": "Ada"}) assert created.status_code == 201
uid = created.json()["id"] fetched = requests.get(f"{base_url}/users/{uid}") assert fetched.status_code == 200 assert fetched.json()["name"] == "Ada"*** Settings ***Library RequestsLibrary
*** Variables ***${BASE_URL} http://localhost:8000
*** Test Cases ***Tao Roi Doc Lai User Create Session api ${BASE_URL} ${body}= Create Dictionary name=Ada ${created}= POST On Session api /users json=${body} Should Be Equal As Integers ${created.status_code} 201 ${uid}= Set Variable ${created.json()}[id] ${fetched}= GET On Session api /users/${uid} Should Be Equal ${fetched.json()}[name] Adafunc TestCreateUser(t *testing.T) { body := strings.NewReader(`{"name":"Ada"}`) res, err := http.Post(baseURL+"/users", "application/json", body) if err != nil { t.Fatalf("request failed: %v", err) } defer res.Body.Close() if res.StatusCode != http.StatusCreated { t.Fatalf("want 201, got %d", res.StatusCode) }}import static io.restassured.RestAssured.*;import static org.hamcrest.Matchers.*;
@Testvoid createsUser() { given() .contentType(ContentType.JSON) .body("{\"name\":\"Ada\"}") .when() .post("/users") .then() .statusCode(201) .body("name", equalTo("Ada"));}[Fact]public async Task CreatesUser(){ var client = new RestClient(baseUrl); var req = new RestRequest("/users", Method.Post) .AddJsonBody(new { Name = "Ada" });
var res = await client.ExecuteAsync(req); Assert.Equal(HttpStatusCode.Created, res.StatusCode);}Anti-patterns & best practices
Section titled “Anti-patterns & best practices”- Assert on the contract, not the implementation. Check status, schema and semantics the client was promised, not private field ordering.
- Validate the schema automatically. Run Schemathesis, Dredd or JSON-schema checks so coverage scales beyond hand-written cases.
- Test the unhappy paths. Empty bodies, wrong types, missing auth, oversized payloads and rate limits are where APIs break.
- Keep collections in version control. Bruno and Newman make collections diffable; stop sharing exported Postman JSON by email.
- Seed and clean via the API itself where possible, and isolate data per run so tests are order-independent.
- Do not chase 100 percent endpoint coverage. Prioritise endpoints on critical client paths.
Self-assessment quiz
Section titled “Self-assessment quiz”Self-assessment quiz
Pick one answer per question, then press Check answers.