Evaluating CodeRabbit? Same review, plus real test runs. See why

Automation Testing14 min readUpdated September 18, 2026

Automated API Testing: Example, Assertions and CI

S
Technical Writer, Qodex
A Playwright API test that requests a post and asserts the status, the content type and the body, followed by those four checks and a footer showing the suite passed in CI
Part of our API Testing guide. Read the guide

Automated API testing uses software to send requests to an API and check each response against explicit expectations without a person repeating the steps. A useful test controls its environment and data, then asserts the status code, headers, response shape and business values. Run the same test on every relevant code change in CI so regressions fail before release.

Qodex does this from your spec, a Postman collection, a spreadsheet or one sentence: runnable scenarios, run against every pull request preview, replayed for $0 in model spend. See Qodex API testing.

What is automated API testing?

An API is the request and response boundary of a service. Automated API testing sends a request to that boundary, reads the response, and compares it with expectations you wrote down in advance. Nothing is clicked. Nothing is eyeballed. The test either passes or it fails with a reason.

Two things often get confused with it. The first is manual API testing, where a person sends requests from a client such as Postman or curl and reads the result. That is still API testing, and it is still useful for exploring a new endpoint. It is not automated, because it needs a person every time. A one-off curl command can become a starting script with the curl converter, but regression coverage still needs assertions, repeatable data and CI. The second is API-driven automation, where an API is used as the control surface for some other job: creating test data, resetting a database, or seeding accounts before a browser test. That is automation through an API, not a test of the API.

The rest of this guide builds one automated test, adds the assertions that make it worth having, and runs it in continuous integration. If you need the broader discipline first, read our guide to API testing.

A runnable automated API testing example

This is a complete package: install commands, a configuration file, one test file, and the command that runs it. It was run again on 16 September 2026 against a public demo API and it passes; the output below is that run. Playwright is the tool here because its test runner ships with an HTTP client, an assertion library and configuration in one dependency, so the example has nothing hidden in a setup step. The same shape works in any framework.

Start in an empty directory and install the runner. The version is pinned so the example behaves the same for you as it did here.

npm init -y
npm install -D @playwright/test@1.63.0
npm pkg set scripts.test:api="playwright test"

Playwright 1.63.0 is the current release, it is Apache-2.0 licensed, and it requires Node 20 or later. Source: the npm registry entry for @playwright/test, read 15 September 2026.

Next, the configuration. This is the file that stops environment values from being hard-coded into tests. It sets a base URL that every request is resolved against and a header that every request carries, and it reads the base URL from an environment variable so the same suite can point at a local server, a preview deployment or staging. Tests that create or delete data belong in those environments, never against production.

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  use: {
    baseURL: process.env.API_BASE_URL || 'https://jsonplaceholder.typicode.com',
    extraHTTPHeaders: { Accept: 'application/json' },
  },
});

The default target is JSONPlaceholder, a free fake API that documents a GET on /posts/1 returning an object with id, title, body and userId. Its writes are fake and nothing persists, so it is safe for a read-only demo and wrong for anything else. Point API_BASE_URL at your own service as soon as the shape of the test makes sense. Source: the JSONPlaceholder guide, read 15 September 2026.

Now the test itself, in tests/posts.spec.ts.

// tests/posts.spec.ts
import { test, expect } from '@playwright/test';

test('GET /posts/1 returns the expected post', async ({ request }) => {
  const response = await request.get('/posts/1');
  expect(response.status()).toBe(200);
  expect(response.headers()['content-type']).toContain('application/json');

  const body = await response.json();
  expect(body).toEqual(expect.objectContaining({ id: 1, userId: 1 }));
  expect(body.title).toEqual(expect.any(String));
});

Four assertions, each doing a different job.

  • Status. A 200 says the request was understood and answered. It is the cheapest signal there is and the weakest on its own, because a broken service can return 200 with an empty body.

  • Content type. The header check catches the failure where an API answers with an HTML error page or a login redirect while still reporting success. Checking that the header contains application/json rather than equalling it allows the charset suffix that many services append.

  • Shape. objectContaining asserts that id and userId are present and correct without asserting the whole object. A strict full-body comparison would break every time an unrelated field is added, which is how suites start getting ignored.

  • Type, not value. The title is demo text that nobody promises to keep stable, so the test asserts that it is a string. Assert exact values only where the value is part of the contract, such as a price, a status field or an identifier you created in the same test.

Run it.

npm run test:api

The pass looks like this.

Running 1 test using 1 worker

  ✓  1 tests/posts.spec.ts:3:5 › GET /posts/1 returns the expected post (732ms)

  1 passed (939ms)

When an assertion fails, the runner prints the expected and received values, the file and the line, so a red build names the broken thing rather than pointing at a suite. That is worth checking once on purpose. Change the expected status to 201 and run it again, so you know what a failure looks like before a real one arrives.

Growing this into a suite means more files in tests/, not a bigger file. One spec per resource keeps failures readable and lets the runner parallelize. Tests that need a logged-in caller get a token from a setup step and pass it through the configuration rather than repeating a login in every file. Tests that need data create it in the test and delete it at the end, so the suite can run twice in a row against the same environment and give the same answer both times. Point that suite at a disposable environment, because creating and deleting records is not something to do in production.

That is the whole loop: a request, assertions that mean something, and configuration that keeps the environment out of the test body. Everything after this is repetition of the same pattern across more endpoints, plus the discipline of running it automatically. Playwright's request fixture picks up baseURL and extraHTTPHeaders from the configuration file, which is why the test says /posts/1 and never names a host. Source: Playwright API testing and test configuration documentation, read 15 September 2026.

Run the API test in CI

A suite that runs when someone remembers to run it is not automation. Commit the tests and the lockfile, then let the pipeline run them on every pull request and every push to the main branch.

# .github/workflows/api-tests.yml
name: API tests
on:
  pull_request:
  push:
    branches: [main]
jobs:
  api-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v7
        with:
          node-version: 24.x
          cache: npm
      - run: npm ci
      - run: npm run test:api
        env:
          API_BASE_URL: https://jsonplaceholder.typicode.com

Five details in that file carry the weight.

  • Triggers. pull_request gives a reviewer the result before the merge, which is the point. The push trigger on main catches anything that lands another way.

  • Node version. 24.x is an Active LTS line, and Node recommends Active LTS or Maintenance LTS for production work. Pin the line, not a patch, so security fixes arrive without a pull request. Source: Node.js previous releases, read 15 September 2026.

  • npm ci, not npm install. It installs the exact versions in the lockfile and does not update it, so the CI run and your machine test the same dependency tree. The first local install is what creates that lockfile. Source: GitHub's Node.js build and test tutorial, read 15 September 2026.

  • Environment through variables. API_BASE_URL is injected by the job. Anything secret, such as a token or an API key, belongs in repository secrets and is referenced the same way, never committed and never printed in a log line.

  • Failure and timeout. A failing assertion exits non-zero, which fails the step and the job. The ten minute timeout stops a hung request from holding a runner for an hour.

Point the job at whatever environment the change actually lives on. If your platform builds a preview deployment per pull request, set API_BASE_URL to that preview URL. The suite then tests the code in the branch instead of yesterday's staging build. Where there is no preview, run against staging on merge and accept that the feedback arrives later.

Decide in advance what a red build means. A failing assertion should block the merge, otherwise the job is a notification and people learn to scroll past it. The failure output stays in the job log, so whoever picks it up reads the expected and received values there instead of rerunning the suite locally.

Keep this job separate from your browser tests. It is fast, it has no browser download, and its failures point at one service rather than at a whole user journey. For deployment patterns beyond this minimal workflow, read continuous API testing in CI/CD pipelines.

What should an automated API test assert?

Weak assertions are the most common way an API suite becomes decorative. It runs, it is green, and it would stay green through a serious regression. Six layers are worth writing.

  • Status codes. Assert the specific code, not merely success. HTTP distinguishes 200 from 201 and 401 from 403 for a reason, and a client depends on the difference. Source: RFC 9110, read 15 September 2026.

  • Headers. Content type first. Add cache, rate limit and correlation headers where clients rely on them.

  • Shape or schema. Check that required fields are present and typed. If you publish an OpenAPI document, validate responses against it: OpenAPI is a standard, language-agnostic interface description for HTTP APIs, and the current version is 3.2.1, which makes it the contract both sides already agreed to. Source: the OpenAPI specification, read 15 September 2026.

  • Business values. The total is the sum. The discount applied. The created record comes back with the identifier you can then fetch. This is the layer that catches real bugs, and it is the layer most suites skip.

  • Negative and authorization cases. Send a malformed payload and assert a 400 with a useful error body. Request another user's record and assert it is refused. Broken object level authorization, broken authentication, unrestricted resource consumption and broken function level authorization all sit in the OWASP API Security Top 10 for 2023, and every one of them is testable with a request your suite can already make. Source: OWASP API Security Top 10, 2023 edition, read 15 September 2026. For dedicated tooling in this area, see our roundup of API security testing tools.

  • Latency budgets, sparingly. Assert a response time only where a number is part of the promise, and set it against a measured baseline. A shared CI runner is noisy, so a tight timing assertion there buys you flaky failures instead of information.

Which API tests should you automate?

Automate what is repetitive, stable and expensive to miss. Leave the rest to people.

Start with regression checks on endpoints that are already live and already agreed. Their behavior is settled, so the test tells you about a change in the code rather than a change of mind. Add contract checks next, especially where another team or an external customer consumes the API, because a removed field or a renamed enum breaks a client you cannot see. Authorization tests come third and never get written unless you plan them: one test per role per sensitive endpoint, asserting both the allowed and the refused case.

Then negative cases, which are cheap to write and catch the sloppy error handling that turns a bad request into a 500. Finally, multi-step workflows: create a resource, read it back, change it, delete it, and assert the state at each step. These catch the bugs that single-endpoint tests cannot see, because the failure lives in the sequence.

Hold back on three things. Endpoints that are still being designed will change under you, so test them by hand until the shape settles. Exploratory work, where you are trying to find out what an API does at its edges, is a human activity by definition. And anything whose expected result nobody can state is not a test yet, it is a question.

Automated execution is not the same as automated judgment. The suite runs itself; deciding what deserves a test, and reading a failure that turns out to be a changed requirement, still needs an engineer.

Choose an API automation approach

The choice is about how tests get authored, configured and run, not about which vendor has the longest feature list. Five approaches cover most teams. All prices and vendor descriptions below were read on 15 September 2026.

ApproachExampleBest forAuthoring and assertionsEnvironment configurationCI commandLicense or published price
Code-first, TypeScriptPlaywright 1.63.0Node teams who want one dependency for requests, assertions and reportingTest files in TypeScript, expect assertionsbaseURL and headers in playwright.config.ts, read from env varsnpm run test:apiApache-2.0, free
Code-first, JavaREST Assured 6.0.1Java teams with an existing JUnit or TestNG suiteJava test classes with a fluent request and response syntaxBuild profiles and system propertiesThe project's own Maven or Gradle test taskApache-2.0
Collection-firstPostman plus NewmanTeams who already keep requests in shared collectionsRequests in the app, assertions written as scriptsPostman environments, exported alongside the collectionNewman CLI step, but see the limit belowFree $0, Solo $9, Team $19 per user, Enterprise $49 per user, monthly, billed annually
PerformanceGrafana k6Load, stress and soak runs against the same endpointsJavaScript or TypeScript scripts with thresholdsScript options and environment variablesThe k6 CLI in a pipeline stepAGPL-3.0
Agent-authoredQodexTeams who want scenarios written for them and kept currentA spec, a Postman collection, a spreadsheet or one sentence in; standard Playwright and HTTP code out, synced to git and exportable; every failure is classified before it reaches youTests parameterized per environmentRuns against every pull request previewVendor claim, see the product page

Each row has a catch. Playwright and k6 assume someone on the team writes code. REST Assured 6.0.0 raised the baseline to Java 17 and 6.0.1 followed on 10 July 2026, so an older stack needs an upgrade first. The Postman route has the sharpest limit right now. Postman states that Newman is not compatible with the collection v3 format used in Postman v12 and later, and points teams at the Postman CLI instead, so check which format your collections are in before you wire Newman into a pipeline. k6 is documented for load, stress and soak testing rather than for functional assertions. Qodex is the vendor of this site, and its claims here are its own.

Three questions settle it for most teams. What language does the team already write and review? Where do the requests live today, in code or in a shared collection? And is the thing you need to prove correctness or capacity? A Node or Python team with no existing collections should write code-first tests and stop shopping. A team whose requests already sit in a maintained collection should keep them there and get them running in CI before rewriting anything.

Mixing is normal and usually correct. Functional and contract checks belong in a code-first suite on every pull request. A load script runs on a schedule or before a release, and collections stay for exploration and for the people who do not write tests. What matters is that one of them gates the merge.

Compare code-first and client-first options in our guide to API testing tools. Java teams can follow the REST Assured API test automation tutorial.

Common failures and how to prevent them

API suites usually rot in the same eight ways.

  • Hard-coded hosts and tokens. The fix is the configuration file above: one place for the base URL, environment variables for anything that differs between environments, secrets from the CI store.

  • Shared mutable data. Two tests editing the same record pass alone and fail together. Have each test create what it needs and clean up after itself.

  • Order dependence. A test that only works after another test ran will break the day someone runs it in parallel. Make every test self-sufficient.

  • Third-party dependencies. An outage at a payment provider should not fail your build. Mock what you do not own and test the real integration in a separate, clearly labelled job.

  • Rate limits. Parallel workers hitting a throttled endpoint produce 429s that look like bugs. Use a dedicated test account, lower the worker count for that suite, or stub the limiter.

  • Weak assertions. A suite that only checks for 200 will stay green while the response body goes empty. Add the shape and business value layers.

  • Blanket retries. Retrying every failure hides the real ones. Retry only known-flaky network conditions, and track what retried, because a test that needs three attempts is telling you something.

  • Failures with no evidence. A red build that says "expected 200" and nothing else costs an hour of guessing. Log the request, the response body and a correlation identifier on failure.

The smallest useful loop

Automated API testing is four things, and you have all four above. A request, assertions that would actually catch a regression, configuration that keeps environment values out of the test body, and a CI job that runs it on every change. Build that for one endpoint today, watch it fail once for a real reason, then copy the pattern outward across the endpoints that matter most. For a Playwright-specific implementation, see Playwright API testing, including APIRequestContext, authentication state, UI setup, and CI.

Frequently Asked Questions

What is API in automation testing?

The API is the thing under test: the request and response boundary a service exposes over HTTP. In automation testing, a script sends a request to that boundary and evaluates the response programmatically, instead of a person sending the request from a client and reading the result. No browser or user interface is involved, which is why these tests are fast and stable.

How do you automate API testing step by step?

Install a runner such as @playwright/test. Put the base URL and shared headers in a configuration file that reads from environment variables. Write one test that sends a request and asserts the status, the content type, the response shape and the business values you care about. Run it locally with a single command, commit it with the lockfile, then add a CI job that runs the same command on every pull request.

Can API testing be fully automated?

Execution can be. Choosing what to test, judging the risk in a change, exploring a new endpoint and deciding whether a failure means a bug or a changed requirement still need people. The practical target is that every agreed behavior has a test that runs without a human, while humans keep the work of deciding what should be agreed.

Is API testing manual or automated?

Both, and they do different jobs. Manual API testing is how you explore an unfamiliar endpoint, reproduce a report, or sanity-check something once. Automated API testing is how you make sure the behavior you already agreed on has not broken. The rule of thumb: the first time you check something, do it by hand; the second time you need the same check, write the test.

Can Selenium automate API testing?

Not directly, and it is the wrong tool for it. Selenium drives a browser, so it can exercise a user journey that happens to call an API, but it does not send requests to an endpoint or assert on a response body. For API checks, use an HTTP client or a framework built for requests: Playwright's request fixture, REST Assured in Java, or a Postman collection run from the command line.

Ship continuously. Test continuously.

Qodex explores your app, writes runnable tests, and replays them on every change at zero LLM cost.