API Testing
By the Syncfusion DevTutorials team · Last updated
Write automated unit and integration tests that check status, headers and body on every change, so a fixed bug never comes back.
API testing is the practice of automatically checking that your code and API behave correctly. Tests send requests, call methods, and verify that the results match what you expect. They run every time the code changes, helping you catch bugs before users do.
Instead of manually checking every feature after every change, automated tests perform those checks for you. This makes development faster, safer, and more reliable.
Watch a Refactor Break the App
Last week, someone cleaned up the Brew & Bytes order code. The API still runs, and on the surface everything looks fine. Press Run.
Program.cs — a refactor nobody tested
Unfortunately, a few important things changed:
POST /ordersnow returns200 OK.- The API contract says it should return
201 Created. - The
Locationheader is missing. - The mobile app uses the
Locationheader to open the newly created order.
As a result, users see a blank screen even though the API appears to work. Because nobody tested the API after the change, the bug remained unnoticed.
In simple words: automated tests check your API every time the code changes. They catch small mistakes before customers experience them.
TL;DR
- A test runs code and checks that the result is correct.
- An assertion is a rule that must be true for the test to pass.
- Unit tests check a small piece of code, such as a service or helper method.
- Integration tests send real HTTP requests and verify responses.
- Test invalid input as well as successful requests.
- Give each test its own data.
- Automated tests help prevent old bugs from returning.
- Common tools include xUnit and
WebApplicationFactoryin .NET, and Vitest andapp.request()in Hono.
A Real-World Example
Imagine a coffee shop before opening.
Every morning, the manager checks whether the coffee machine works, whether the receipts print correctly, whether the card reader is online, and whether the latte is prepared correctly. The manager performs these checks every day, even if everything worked yesterday.
Small changes happen all the time, such as different ingredients, different staff, and equipment updates. Tests serve the same purpose for an API. They verify that important features still work whenever code changes.
A healthy test suite contains many unit tests, fewer integration tests, and a small number of end-to-end tests.
What You Will Learn
In this lesson, you will learn:
- Why automated testing is important.
- How unit tests verify business logic.
- How edge-case tests catch bugs.
- How integration tests verify API contracts.
- Why tests should use fresh data.
- Which testing tools are commonly used in .NET and TypeScript.
Step 1: Your First Unit Tests
A unit test checks a small piece of code in isolation. For example, instead of starting a web server, the test creates an OrderService and calls its methods directly.
OrderServiceTests.cs — a tiny test runner
Each test produces one result: ✓ Passed or ✗ Failed.
The tests verify that correct totals are calculated, that invalid menu items are rejected, that quantities outside the allowed range fail, and that the expected error messages are returned.
For example, 2 Lattes cost ₹440, the GST is ₹22, and the total is ₹462. If every assertion succeeds, the summary is: 4 passed, 0 failed
Real testing frameworks do the same job. They find the tests by themselves, run them, and print detailed reports.
Step 2: A Test That Catches a Bug
A passing test suite is valuable because it catches mistakes immediately. Suppose someone changes the GST calculation. The existing tests still pass. To catch the issue, we add a new test that covers a different value.
OrderServiceTests.cs — a test catches a bug
Example: a Blueberry Muffin costs ₹150, and the GST is 5%. The GST should be ₹7.50, which rounds to ₹8, so the total should be ₹158. Instead, the test reports: expected 158, got 157
The bug occurs because integer division removes the decimal portion. The correct calculation rounds properly before producing the final value.
The important lesson is that the test stays in the project even after the bug is fixed. If the bug reappears later, the test immediately fails.
When you fix a bug, first write a test that shows the bug. After the fix, the test should pass. This kind of test is called a regression test, because it stops old bugs from coming back.
Step 3: Contract Checks Over HTTP
Unit tests verify business logic. They do not verify status codes, headers, request routing, or API contracts. For these checks, use integration tests. Integration tests send real HTTP requests to a running API.
Program.cs — the contract checks
The test suite checks scenarios such as:
Successful Creation
Request: POST /orders. Expected: 201 Created and Location: /orders/103.
Validation Error
Request: quantity = 0. Expected: 400 Bad Request and errors.quantity.
Missing Resource
Request: GET /orders/999. Expected: 404 Not Found.
Idempotent Retry
Two requests use the same Idempotency-Key. Expected: the same order is returned both times, and no duplicate order is created.
Read the Created Resource
Request: GET /orders/104. Expected: the correct order is returned, and the data matches the original response.
If the bug from the beginning returns, the test immediately reports: expected 201, got 200
Make a change, run tests, fix failures, and deploy only when all tests pass.
Step 4: Fresh Data for Every Test
Tests should never depend on previous tests. Each test should start with clean data.
SeedTests.cs — the same tests on three data sets
The same tests run against multiple data sets, called seeds. Each seed contains different menu items, different prices, and different expected results.
Example: the Airport kiosk menu has Latte = ₹310. The GST is ₹15.50, which rounds to ₹16, so the expected total is ₹326.
Running tests against different inputs helps prevent hard-coded solutions from passing incorrectly. Each seed also creates a fresh OrderService, so nothing carries over from a previous test.
Never let tests share state. If one test creates data that another test needs, the tests become unreliable. Each test should be able to run on its own and give the same result every time.
The Real Tools
The in-browser runner cannot run test frameworks, so the code below is read-only. Copy it into a real project to try it.
In .NET, xUnit runs the tests, and WebApplicationFactory<Program> starts your whole API in memory. The test sends a real request without opening a network port.
// OrdersApiTests.cs (needs Microsoft.AspNetCore.Mvc.Testing and xunit)
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient client;
public OrdersApiTests(WebApplicationFactory<Program> factory) => client = factory.CreateClient();
[Fact]
public async Task Post_order_returns_201_and_location()
{
var response = await client.PostAsJsonAsync("/orders", new { item = "Latte", quantity = 2 });
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
Assert.StartsWith("/orders/", response.Headers.Location!.ToString());
}
[Fact]
public async Task Quantity_zero_returns_400()
{
var response = await client.PostAsJsonAsync("/orders", new { item = "Latte", quantity = 0 });
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
}
Add public partial class Program { } at the end of Program.cs, so that the test project can see Program. Run the tests with dotnet test.
In Hono, app.request() sends a request straight to your app, with no server. Vitest runs the test file.
// orders.test.ts (needs vitest)
import { describe, expect, it } from "vitest";
import app from "./server";
describe("POST /orders", () => {
it("returns 201 and a Location", async () => {
const res = await app.request("/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ item: "Latte", quantity: 2 }),
});
expect(res.status).toBe(201);
expect(res.headers.get("Location")).toMatch(/^\/orders\//);
});
});
Run it with npx vitest. Both tools print the same kind of summary that you saw above, and both can run in your build pipeline on every push.
Where You See This
- Pull requests: most teams will not merge a change until every test is green.
- Build pipelines run
dotnet testornpx viteston every push. - Public APIs, such as payment providers, keep thousands of contract tests, so that partners never break.
Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
| A test passes alone but fails in the full run | Tests share state, such as one store or one id counter | Create fresh data in each test |
| Users hit errors that the tests never showed | Only the happy path is tested | Add tests for bad input, missing ids and retries |
| A test is always green, even when the code breaks | The test has no assertion | Assert the exact status, value or message |
| An old bug comes back | The fix had no test | Add a regression test with the fix |
A refactor changes 201 to 200 without anyone noticing | Tests check the body but not the status or headers | Assert the status and Location too |
Exercises
What to Remember
- A test calls your code and asserts the answer. Run tests on every change.
- Unit tests check one class. Integration tests check status, headers and body over HTTP.
- Test the bad input and edge cases, not only the happy path.
- Write a regression test for every bug that you fix.
- Give every test fresh data. Tests must not depend on each other.
Next: API Gateway. One front door that routes, secures and limits traffic for all your services.