# API Testing

Course: [API](https://www.devtutorials.ai/learn/building-apis) · Section: Production-Ready APIs
Source: https://www.devtutorials.ai/learn/building-apis/production-ready/api-testing

> 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**.

Unfortunately, a few important things changed:

- `POST /orders` now returns `200 OK`.
- The API contract says it should return `201 Created`.
- The `Location` header is missing.
- The mobile app uses the `Location` header 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 `WebApplicationFactory` in .NET, and **Vitest** and `app.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.

## What You Will Learn

In this lesson, you will learn:

1. Why automated testing is important.
2. How unit tests verify business logic.
3. How edge-case tests catch bugs.
4. How integration tests verify API contracts.
5. Why tests should use fresh data.
6. 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.

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.

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.

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`

## Step 4: Fresh Data for Every Test

Tests should never depend on previous tests. Each test should start with clean data.

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.

```csharp
// 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.

```ts
// 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 test` or `npx vitest` on 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.

var app = builder.Build();

var prices = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120 };
var orders = new Dictionary<int, Order>();
int nextId = 103;

// After last week's "small refactor".
// It used to return 201 Created with a Location header.
// Nobody tested it, so nobody noticed the change.
app.MapPost("/orders", (OrderRequest input) =>
{
    var order = new Order(nextId++, input.Item, input.Quantity, prices[input.Item] * input.Quantity);
    orders[order.Id] = order;
    return Results.Ok(order);
});

app.MapGet("/orders/{id}", (int id) =>
    orders.TryGetValue(id, out var order) ? Results.Ok(order) : Results.NotFound());

app.Run();

record OrderRequest(string Item, int Quantity);
record Order(int Id, string Item, int Quantity, int Total);`;

const prices = new Map<string, number>([["Latte", 220], ["Masala Chai", 120]]);
const orders = new Map<number, { id: number; item: string; quantity: number; total: number }>();
let nextId = 103;

const app = new Hono();

// After last week's "small refactor".
// It used to return 201 Created with a Location header.
// Nobody tested it, so nobody noticed the change.
app.post("/orders", async (c) => {
  const input = await c.req.json<{ item: string; quantity: number }>();
  const order = { id: nextId++, item: input.item, quantity: input.quantity, total: prices.get(input.item)! * input.quantity };
  orders.set(order.id, order);
  return c.json(order, 200);
});

app.get("/orders/:id", (c) => {
  const order = orders.get(Number(c.req.param("id")));
  return order ? c.json(order, 200) : c.body(null, 404);
});

Content-Type: application/json

{"item":"Latte","quantity":2}

###
GET /orders/103`;

int passed = 0, failed = 0;
void Test(string name, Action body)
{
    try { body(); passed++; Console.WriteLine("✓ " + name); }
    catch (Exception e) { failed++; Console.WriteLine("✗ " + name + ": " + e.Message); }
}
void Equal(object expected, object actual)
{
    if (!expected.Equals(actual)) throw new Exception("expected " + expected + ", got " + actual);
}
void Throws(Action action, string message)
{
    try { action(); } catch (Exception e) { Equal(message, e.Message); return; }
    throw new Exception("expected an error, got none");
}

var service = new OrderService();
Test("2 Lattes cost 440 + 22 GST", () => Equal(462, service.Total("Latte", 2)));
Test("1 Masala Chai costs 120 + 6 GST", () => Equal(126, service.Total("Masala Chai", 1)));
Test("unknown item is rejected", () => Throws(() => service.Total("Pizza", 1), "Pizza is not on the menu."));
Test("quantity 11 is rejected", () => Throws(() => service.Total("Latte", 11), "Quantity must be 1 to 10."));
Console.WriteLine(passed + " passed, " + failed + " failed");

// The code under test: plain C#, no web server needed.
class OrderService
{
    Dictionary<string, int> prices = new() { ["Latte"] = 220, ["Masala Chai"] = 120, ["Blueberry Muffin"] = 150 };
    public int Total(string item, int quantity)
    {
        if (!prices.ContainsKey(item)) throw new Exception(item + " is not on the menu.");
        if (quantity < 1 || quantity > 10) throw new Exception("Quantity must be 1 to 10.");
        int subtotal = prices[item] * quantity;
        return subtotal + (subtotal * 5 + 50) / 100; // 5% GST, rounded to the nearest rupee
    }
}`;

let passed = 0, failed = 0;
function test(name: string, body: () => void) {
  try { body(); passed++; console.log("✓ " + name); }
  catch (e) { failed++; console.log("✗ " + name + ": " + (e as Error).message); }
}
function equal(expected: unknown, actual: unknown) {
  if (expected !== actual) throw new Error("expected " + expected + ", got " + actual);
}
function throws(action: () => void, message: string) {
  try { action(); } catch (e) { equal(message, (e as Error).message); return; }
  throw new Error("expected an error, got none");
}

// The code under test: plain TypeScript, no web server needed.
class OrderService {
  prices = new Map<string, number>([["Latte", 220], ["Masala Chai", 120], ["Blueberry Muffin", 150]]);
  total(item: string, quantity: number): number {
    if (!this.prices.has(item)) throw new Error(item + " is not on the menu.");
    if (quantity < 1 || quantity > 10) throw new Error("Quantity must be 1 to 10.");
    const subtotal = this.prices.get(item)! * quantity;
    return subtotal + Math.floor((subtotal * 5 + 50) / 100); // 5% GST, rounded to the nearest rupee
  }
}

const service = new OrderService();
test("2 Lattes cost 440 + 22 GST", () => equal(462, service.total("Latte", 2)));
test("1 Masala Chai costs 120 + 6 GST", () => equal(126, service.total("Masala Chai", 1)));
test("unknown item is rejected", () => throws(() => service.total("Pizza", 1), "Pizza is not on the menu."));
test("quantity 11 is rejected", () => throws(() => service.total("Latte", 11), "Quantity must be 1 to 10."));
console.log(passed + " passed, " + failed + " failed");`;

int passed = 0, failed = 0;
void Test(string name, Action body)
{
    try { body(); passed++; Console.WriteLine("✓ " + name); }
    catch (Exception e) { failed++; Console.WriteLine("✗ " + name + ": " + e.Message); }
}
void Equal(object expected, object actual)
{
    if (!expected.Equals(actual)) throw new Exception("expected " + expected + ", got " + actual);
}
void Throws(Action action, string message)
{
    try { action(); } catch (Exception e) { Equal(message, e.Message); return; }
    throw new Exception("expected an error, got none");
}

var service = new OrderService();
Test("2 Lattes cost 440 + 22 GST", () => Equal(462, service.Total("Latte", 2)));
Test("1 Masala Chai costs 120 + 6 GST", () => Equal(126, service.Total("Masala Chai", 1)));
Test("1 Blueberry Muffin costs 150 + 7.5 GST, rounded up", () => Equal(158, service.Total("Blueberry Muffin", 1)));
Test("unknown item is rejected", () => Throws(() => service.Total("Pizza", 1), "Pizza is not on the menu."));
Test("quantity 11 is rejected", () => Throws(() => service.Total("Latte", 11), "Quantity must be 1 to 10."));
Console.WriteLine(passed + " passed, " + failed + " failed");

// The code under test: plain C#, no web server needed.
class OrderService
{
    Dictionary<string, int> prices = new() { ["Latte"] = 220, ["Masala Chai"] = 120, ["Blueberry Muffin"] = 150 };
    public int Total(string item, int quantity)
    {
        if (!prices.ContainsKey(item)) throw new Exception(item + " is not on the menu.");
        if (quantity < 1 || quantity > 10) throw new Exception("Quantity must be 1 to 10.");
        int subtotal = prices[item] * quantity;
        return subtotal + subtotal * 5 / 100; // 5% GST... but integer division cuts off the .5
    }
}`;

let passed = 0, failed = 0;
function test(name: string, body: () => void) {
  try { body(); passed++; console.log("✓ " + name); }
  catch (e) { failed++; console.log("✗ " + name + ": " + (e as Error).message); }
}
function equal(expected: unknown, actual: unknown) {
  if (expected !== actual) throw new Error("expected " + expected + ", got " + actual);
}
function throws(action: () => void, message: string) {
  try { action(); } catch (e) { equal(message, (e as Error).message); return; }
  throw new Error("expected an error, got none");
}

// The code under test: plain TypeScript, no web server needed.
class OrderService {
  prices = new Map<string, number>([["Latte", 220], ["Masala Chai", 120], ["Blueberry Muffin", 150]]);
  total(item: string, quantity: number): number {
    if (!this.prices.has(item)) throw new Error(item + " is not on the menu.");
    if (quantity < 1 || quantity > 10) throw new Error("Quantity must be 1 to 10.");
    const subtotal = this.prices.get(item)! * quantity;
    return subtotal + Math.floor(subtotal * 5 / 100); // 5% GST... but floor cuts off the .5
  }
}

const service = new OrderService();
test("2 Lattes cost 440 + 22 GST", () => equal(462, service.total("Latte", 2)));
test("1 Masala Chai costs 120 + 6 GST", () => equal(126, service.total("Masala Chai", 1)));
test("1 Blueberry Muffin costs 150 + 7.5 GST, rounded up", () => equal(158, service.total("Blueberry Muffin", 1)));
test("unknown item is rejected", () => throws(() => service.total("Pizza", 1), "Pizza is not on the menu."));
test("quantity 11 is rejected", () => throws(() => service.total("Latte", 11), "Quantity must be 1 to 10."));
console.log(passed + " passed, " + failed + " failed");`;

var app = builder.Build();

var prices = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120 };
var orders = new Dictionary<int, Order>();
var byKey = new Dictionary<string, Order>(); // Idempotency-Key -> first order
int nextId = 103;

// The fixed API. Each request in the box below is one check in a test suite.
app.MapPost("/orders", (OrderRequest input, HttpContext context) =>
{
    string key = context.Request.Headers["Idempotency-Key"].ToString();
    if (key != "" && byKey.TryGetValue(key, out var first))
        return Results.Created("/orders/" + first.Id, first); // a retry: same answer
    if (!prices.ContainsKey(input.Item) || input.Quantity < 1 || input.Quantity > 10)
        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            ["quantity"] = new[] { "Quantity must be between 1 and 10." }
        });
    var order = new Order(nextId++, input.Item, input.Quantity, prices[input.Item] * input.Quantity);
    orders[order.Id] = order;
    if (key != "") byKey[key] = order;
    return Results.Created("/orders/" + order.Id, order);
});

app.MapGet("/orders/{id}", (int id) =>
    orders.TryGetValue(id, out var order) ? Results.Ok(order) : Results.NotFound());

app.Run();

record OrderRequest(string Item, int Quantity);
record Order(int Id, string Item, int Quantity, int Total);`;

type Order = { id: number; item: string; quantity: number; total: number };
const prices = new Map<string, number>([["Latte", 220], ["Masala Chai", 120]]);
const orders = new Map<number, Order>();
const byKey = new Map<string, Order>(); // Idempotency-Key -> first order
let nextId = 103;

const app = new Hono();

// The fixed API. Each request in the box below is one check in a test suite.
app.post("/orders", async (c) => {
  const input = await c.req.json<{ item: string; quantity: number }>();
  const key = c.req.header("Idempotency-Key") ?? "";
  const first = byKey.get(key);
  if (key !== "" && first) {
    c.header("Location", "/orders/" + first.id); // a retry: same answer
    return c.json(first, 201);
  }
  if (!prices.has(input.item) || !(input.quantity >= 1 && input.quantity <= 10)) {
    const problem = { type: "https://tools.ietf.org/html/rfc9110#section-15.5.1", title: "One or more validation errors occurred.", status: 400, errors: { quantity: ["Quantity must be between 1 and 10."] } };
    return c.body(JSON.stringify(problem), 400, { "Content-Type": "application/problem+json" });
  }
  const order = { id: nextId++, item: input.item, quantity: input.quantity, total: prices.get(input.item)! * input.quantity };
  orders.set(order.id, order);
  if (key !== "") byKey.set(key, order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

app.get("/orders/:id", (c) => {
  const order = orders.get(Number(c.req.param("id")));
  return order ? c.json(order, 200) : c.body(null, 404);
});

Content-Type: application/json

{"item":"Latte","quantity":2}

###
POST /orders
Content-Type: application/json

{"item":"Latte","quantity":0}

###
GET /orders/999

###
POST /orders
Idempotency-Key: k-55
Content-Type: application/json

{"item":"Masala Chai","quantity":1}

###
POST /orders
Idempotency-Key: k-55
Content-Type: application/json

{"item":"Masala Chai","quantity":1}

###
GET /orders/104`;

var seeds = new[]
{
    new Seed("Brew & Bytes", new() { ["Latte"] = 220, ["Cold Coffee"] = 180 }, "Latte", 2, 462),
    new Seed("Festival stall", new() { ["Latte"] = 250, ["Cold Coffee"] = 200 }, "Cold Coffee", 3, 630),
    new Seed("Airport kiosk", new() { ["Latte"] = 310 }, "Latte", 1, 326),
};

foreach (var seed in seeds)
{
    // A NEW service for every seed, so no state leaks from one run to the next.
    var service = new OrderService(seed.Menu);
    int passed = 0, failed = 0;
    void Check(bool ok) { if (ok) passed++; else failed++; }
    Check(service.Total(seed.Item, seed.Quantity) == seed.Expected);
    Check(Rejects(() => service.Total("Pizza", 1)));
    Check(Rejects(() => service.Total(seed.Item, 0)));
    Console.WriteLine(seed.Name + ": " + passed + " passed, " + failed + " failed");
}

bool Rejects(Action action)
{
    try { action(); return false; } catch (Exception) { return true; }
}

record Seed(string Name, Dictionary<string, int> Menu, string Item, int Quantity, int Expected);

class OrderService(Dictionary<string, int> prices)
{
    public int Total(string item, int quantity)
    {
        if (!prices.ContainsKey(item)) throw new Exception(item + " is not on the menu.");
        if (quantity < 1 || quantity > 10) throw new Exception("Quantity must be 1 to 10.");
        int subtotal = prices[item] * quantity;
        return subtotal + (subtotal * 5 + 50) / 100;
    }
}`;

  constructor(private prices: Map<string, number>) {}
  total(item: string, quantity: number): number {
    if (!this.prices.has(item)) throw new Error(item + " is not on the menu.");
    if (quantity < 1 || quantity > 10) throw new Error("Quantity must be 1 to 10.");
    const subtotal = this.prices.get(item)! * quantity;
    return subtotal + Math.floor((subtotal * 5 + 50) / 100);
  }
}

// Three data sets ("seeds"). Each one has its own menu and its own expected total.
const seeds = [
  { name: "Brew & Bytes", menu: new Map([["Latte", 220], ["Cold Coffee", 180]]), item: "Latte", quantity: 2, expected: 462 },
  { name: "Festival stall", menu: new Map([["Latte", 250], ["Cold Coffee", 200]]), item: "Cold Coffee", quantity: 3, expected: 630 },
  { name: "Airport kiosk", menu: new Map([["Latte", 310]]), item: "Latte", quantity: 1, expected: 326 },
];

function rejects(action: () => void): boolean {
  try { action(); return false; } catch { return true; }
}

for (const seed of seeds) {
  // A NEW service for every seed, so no state leaks from one run to the next.
  const service = new OrderService(seed.menu);
  let passed = 0, failed = 0;
  const check = (ok: boolean) => { if (ok) passed++; else failed++; };
  check(service.total(seed.item, seed.quantity) === seed.expected);
  check(rejects(() => service.total("Pizza", 1)));
  check(rejects(() => service.total(seed.item, 0)));
  console.log(seed.name + ": " + passed + " passed, " + failed + " failed");
}`;

  {
    prompt: "The test for quantity 0 is empty, so it proves nothing. Write the missing assertion.",
    hint: "Call Throws with a call to Total for Latte and quantity 0, and the message Quantity must be 1 to 10.",
    explain: "A test with no assertion always passes. Now the test fails if someone removes the quantity check.",
    csharp: { starter: `// This runner also counts assertions. A test with none proves nothing.
int asserts = 0;
void Test(string name, Action body)
{
    asserts = 0;
    try
    {
        body();
        Console.WriteLine((asserts == 0 ? "? " + name + ": no assertions" : "✓ " + name));
    }
    catch (Exception e) { Console.WriteLine("✗ " + name + ": " + e.Message); }
}
void Throws(Action action, string message)
{
    asserts++;
    try { action(); } catch (Exception e) { if (e.Message == message) return; throw new Exception("wrong error: " + e.Message); }
    throw new Exception("expected an error, got none");
}

var service = new OrderService();
Test("quantity 11 is rejected", () => Throws(() => service.Total("Latte", 11), "Quantity must be 1 to 10."));
Test("quantity 0 is rejected", () =>
{
    // TODO: write the missing assertion.
    // Call Throws with service.Total("Latte", 0) and the message "Quantity must be 1 to 10."
});

class OrderService
{
    public int Total(string item, int quantity)
    {
        if (quantity < 1 || quantity > 10) throw new Exception("Quantity must be 1 to 10.");
        return 220 * quantity;
    }
}`, solution: `// This runner also counts assertions. A test with none proves nothing.
int asserts = 0;
void Test(string name, Action body)
{
    asserts = 0;
    try
    {
        body();
        Console.WriteLine((asserts == 0 ? "? " + name + ": no assertions" : "✓ " + name));
    }
    catch (Exception e) { Console.WriteLine("✗ " + name + ": " + e.Message); }
}
void Throws(Action action, string message)
{
    asserts++;
    try { action(); } catch (Exception e) { if (e.Message == message) return; throw new Exception("wrong error: " + e.Message); }
    throw new Exception("expected an error, got none");
}

var service = new OrderService();
Test("quantity 11 is rejected", () => Throws(() => service.Total("Latte", 11), "Quantity must be 1 to 10."));
Test("quantity 0 is rejected", () =>
{
    // The missing assertion: quantity 0 must throw this exact message.
    Throws(() => service.Total("Latte", 0), "Quantity must be 1 to 10.");
});

class OrderService
{
    public int Total(string item, int quantity)
    {
        if (quantity < 1 || quantity > 10) throw new Exception("Quantity must be 1 to 10.");
        return 220 * quantity;
    }
}`, mode: "console" },
    react: { starter: `// This runner also counts assertions. A test with none proves nothing.
let asserts = 0;
function test(name: string, body: () => void) {
  asserts = 0;
  try {
    body();
    console.log(asserts === 0 ? "? " + name + ": no assertions" : "✓ " + name);
  } catch (e) { console.log("✗ " + name + ": " + (e as Error).message); }
}
function throws(action: () => void, message: string) {
  asserts++;
  try { action(); } catch (e) { if ((e as Error).message === message) return; throw new Error("wrong error: " + (e as Error).message); }
  throw new Error("expected an error, got none");
}

class OrderService {
  total(item: string, quantity: number): number {
    if (quantity < 1 || quantity > 10) throw new Error("Quantity must be 1 to 10.");
    return 220 * quantity;
  }
}

const service = new OrderService();
test("quantity 11 is rejected", () => throws(() => service.total("Latte", 11), "Quantity must be 1 to 10."));
test("quantity 0 is rejected", () => {
  // TODO: write the missing assertion.
  // Call throws with service.total("Latte", 0) and the message "Quantity must be 1 to 10."
});`, solution: `// This runner also counts assertions. A test with none proves nothing.
let asserts = 0;
function test(name: string, body: () => void) {
  asserts = 0;
  try {
    body();
    console.log(asserts === 0 ? "? " + name + ": no assertions" : "✓ " + name);
  } catch (e) { console.log("✗ " + name + ": " + (e as Error).message); }
}
function throws(action: () => void, message: string) {
  asserts++;
  try { action(); } catch (e) { if ((e as Error).message === message) return; throw new Error("wrong error: " + (e as Error).message); }
  throw new Error("expected an error, got none");
}

class OrderService {
  total(item: string, quantity: number): number {
    if (quantity < 1 || quantity > 10) throw new Error("Quantity must be 1 to 10.");
    return 220 * quantity;
  }
}

const service = new OrderService();
test("quantity 11 is rejected", () => throws(() => service.total("Latte", 11), "Quantity must be 1 to 10."));
test("quantity 0 is rejected", () => {
  // The missing assertion: quantity 0 must throw this exact message.
  throws(() => service.total("Latte", 0), "Quantity must be 1 to 10.");
});`, mode: "console" }
  },
  {
    prompt: "The runner prints only failures. Make it count passed and failed tests, print a ✓ for each pass, and print the summary line at the end.",
    hint: "Add two counters. Add 1 to passed after body() runs, and add 1 to failed in the catch. Print them after the last test.",
    explain: "A summary tells you how many tests ran. Silence could mean that all tests passed, or that nothing ran at all.",
    csharp: { starter: `// TODO: this runner only prints failures. You cannot tell if 1 test ran or 100.
// Count passed and failed, print "✓ name" for a pass,
// and print the summary line at the end: "4 passed, 1 failed".
void Test(string name, Action body)
{
    try { body(); }
    catch (Exception e) { Console.WriteLine("✗ " + name + ": " + e.Message); }
}
void Equal(object expected, object actual)
{
    if (!expected.Equals(actual)) throw new Exception("expected " + expected + ", got " + actual);
}
void Throws(Action action, string message)
{
    try { action(); } catch (Exception e) { Equal(message, e.Message); return; }
    throw new Exception("expected an error, got none");
}

var service = new OrderService();
Test("2 Lattes cost 440 + 22 GST", () => Equal(462, service.Total("Latte", 2)));
Test("1 Masala Chai costs 120 + 6 GST", () => Equal(126, service.Total("Masala Chai", 1)));
Test("1 Blueberry Muffin costs 150 + 7.5 GST, rounded up", () => Equal(158, service.Total("Blueberry Muffin", 1)));
Test("unknown item is rejected", () => Throws(() => service.Total("Pizza", 1), "Pizza is not on the menu."));
Test("quantity 11 is rejected", () => Throws(() => service.Total("Latte", 11), "Quantity must be 1 to 10."));

// The code under test: plain C#, no web server needed.
class OrderService
{
    Dictionary<string, int> prices = new() { ["Latte"] = 220, ["Masala Chai"] = 120, ["Blueberry Muffin"] = 150 };
    public int Total(string item, int quantity)
    {
        if (!prices.ContainsKey(item)) throw new Exception(item + " is not on the menu.");
        if (quantity < 1 || quantity > 10) throw new Exception("Quantity must be 1 to 10.");
        int subtotal = prices[item] * quantity;
        return subtotal + subtotal * 5 / 100; // 5% GST... but integer division cuts off the .5
    }
}`, solution: `// The runner counts every result and prints a ✓ for each pass.
int passed = 0, failed = 0;
void Test(string name, Action body)
{
    try { body(); passed++; Console.WriteLine("✓ " + name); }
    catch (Exception e) { failed++; Console.WriteLine("✗ " + name + ": " + e.Message); }
}
void Equal(object expected, object actual)
{
    if (!expected.Equals(actual)) throw new Exception("expected " + expected + ", got " + actual);
}
void Throws(Action action, string message)
{
    try { action(); } catch (Exception e) { Equal(message, e.Message); return; }
    throw new Exception("expected an error, got none");
}

var service = new OrderService();
Test("2 Lattes cost 440 + 22 GST", () => Equal(462, service.Total("Latte", 2)));
Test("1 Masala Chai costs 120 + 6 GST", () => Equal(126, service.Total("Masala Chai", 1)));
Test("1 Blueberry Muffin costs 150 + 7.5 GST, rounded up", () => Equal(158, service.Total("Blueberry Muffin", 1)));
Test("unknown item is rejected", () => Throws(() => service.Total("Pizza", 1), "Pizza is not on the menu."));
Test("quantity 11 is rejected", () => Throws(() => service.Total("Latte", 11), "Quantity must be 1 to 10."));
Console.WriteLine(passed + " passed, " + failed + " failed");

// The code under test: plain C#, no web server needed.
class OrderService
{
    Dictionary<string, int> prices = new() { ["Latte"] = 220, ["Masala Chai"] = 120, ["Blueberry Muffin"] = 150 };
    public int Total(string item, int quantity)
    {
        if (!prices.ContainsKey(item)) throw new Exception(item + " is not on the menu.");
        if (quantity < 1 || quantity > 10) throw new Exception("Quantity must be 1 to 10.");
        int subtotal = prices[item] * quantity;
        return subtotal + subtotal * 5 / 100; // 5% GST... but integer division cuts off the .5
    }
}`, mode: "console" },
    react: { starter: `// TODO: this runner only prints failures. You cannot tell if 1 test ran or 100.
// Count passed and failed, print "✓ name" for a pass,
// and print the summary line at the end: "4 passed, 1 failed".
function test(name: string, body: () => void) {
  try { body(); }
  catch (e) { console.log("✗ " + name + ": " + (e as Error).message); }
}
function equal(expected: unknown, actual: unknown) {
  if (expected !== actual) throw new Error("expected " + expected + ", got " + actual);
}
function throws(action: () => void, message: string) {
  try { action(); } catch (e) { equal(message, (e as Error).message); return; }
  throw new Error("expected an error, got none");
}

// The code under test: plain TypeScript, no web server needed.
class OrderService {
  prices = new Map<string, number>([["Latte", 220], ["Masala Chai", 120], ["Blueberry Muffin", 150]]);
  total(item: string, quantity: number): number {
    if (!this.prices.has(item)) throw new Error(item + " is not on the menu.");
    if (quantity < 1 || quantity > 10) throw new Error("Quantity must be 1 to 10.");
    const subtotal = this.prices.get(item)! * quantity;
    return subtotal + Math.floor(subtotal * 5 / 100); // 5% GST... but floor cuts off the .5
  }
}

const service = new OrderService();
test("2 Lattes cost 440 + 22 GST", () => equal(462, service.total("Latte", 2)));
test("1 Masala Chai costs 120 + 6 GST", () => equal(126, service.total("Masala Chai", 1)));
test("1 Blueberry Muffin costs 150 + 7.5 GST, rounded up", () => equal(158, service.total("Blueberry Muffin", 1)));
test("unknown item is rejected", () => throws(() => service.total("Pizza", 1), "Pizza is not on the menu."));
test("quantity 11 is rejected", () => throws(() => service.total("Latte", 11), "Quantity must be 1 to 10."));`, solution: `// The runner counts every result and prints a ✓ for each pass.
let passed = 0, failed = 0;
function test(name: string, body: () => void) {
  try { body(); passed++; console.log("✓ " + name); }
  catch (e) { failed++; console.log("✗ " + name + ": " + (e as Error).message); }
}
function equal(expected: unknown, actual: unknown) {
  if (expected !== actual) throw new Error("expected " + expected + ", got " + actual);
}
function throws(action: () => void, message: string) {
  try { action(); } catch (e) { equal(message, (e as Error).message); return; }
  throw new Error("expected an error, got none");
}

// The code under test: plain TypeScript, no web server needed.
class OrderService {
  prices = new Map<string, number>([["Latte", 220], ["Masala Chai", 120], ["Blueberry Muffin", 150]]);
  total(item: string, quantity: number): number {
    if (!this.prices.has(item)) throw new Error(item + " is not on the menu.");
    if (quantity < 1 || quantity > 10) throw new Error("Quantity must be 1 to 10.");
    const subtotal = this.prices.get(item)! * quantity;
    return subtotal + Math.floor(subtotal * 5 / 100); // 5% GST... but floor cuts off the .5
  }
}

const service = new OrderService();
test("2 Lattes cost 440 + 22 GST", () => equal(462, service.total("Latte", 2)));
test("1 Masala Chai costs 120 + 6 GST", () => equal(126, service.total("Masala Chai", 1)));
test("1 Blueberry Muffin costs 150 + 7.5 GST, rounded up", () => equal(158, service.total("Blueberry Muffin", 1)));
test("unknown item is rejected", () => throws(() => service.total("Pizza", 1), "Pizza is not on the menu."));
test("quantity 11 is rejected", () => throws(() => service.total("Latte", 11), "Quantity must be 1 to 10."));
console.log(passed + " passed, " + failed + " failed");`, mode: "console" }
  },
  {
    prompt: "The contract check expects POST /orders to return 201 Created with a Location header. Fix the API so that it passes.",
    hint: "In C#, use Results.Created with the URL /orders/ plus the id. In Hono, set the Location header and use status 201.",
    explain: "Clients rely on 201 and Location to find the new order. A contract test makes sure that a refactor cannot quietly change them.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var prices = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120 };
var orders = new Dictionary<int, Order>();
int nextId = 103;

// TODO: the contract says POST /orders returns 201 Created
// with a Location header like /orders/103.
// Fix the return line so the contract check passes.
app.MapPost("/orders", (OrderRequest input) =>
{
    var order = new Order(nextId++, input.Item, input.Quantity, prices[input.Item] * input.Quantity);
    orders[order.Id] = order;
    return Results.Ok(order);
});

app.MapGet("/orders/{id}", (int id) =>
    orders.TryGetValue(id, out var order) ? Results.Ok(order) : Results.NotFound());

app.Run();

record OrderRequest(string Item, int Quantity);
record Order(int Id, string Item, int Quantity, int Total);`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var prices = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120 };
var orders = new Dictionary<int, Order>();
int nextId = 103;

// The contract says POST /orders returns 201 Created
// with a Location header like /orders/103.
// Created sets both the status and the Location.
app.MapPost("/orders", (OrderRequest input) =>
{
    var order = new Order(nextId++, input.Item, input.Quantity, prices[input.Item] * input.Quantity);
    orders[order.Id] = order;
    return Results.Created("/orders/" + order.Id, order);
});

app.MapGet("/orders/{id}", (int id) =>
    orders.TryGetValue(id, out var order) ? Results.Ok(order) : Results.NotFound());

app.Run();

record OrderRequest(string Item, int Quantity);
record Order(int Id, string Item, int Quantity, int Total);`, mode: "aspnet", requests: `POST /orders
Content-Type: application/json

{"item":"Latte","quantity":2}

###
GET /orders/103` },
    react: { starter: `import { Hono } from "hono";

const prices = new Map<string, number>([["Latte", 220], ["Masala Chai", 120]]);
const orders = new Map<number, { id: number; item: string; quantity: number; total: number }>();
let nextId = 103;

const app = new Hono();

// TODO: the contract says POST /orders returns 201 Created
// with a Location header like /orders/103.
// Fix the return line so the contract check passes.
app.post("/orders", async (c) => {
  const input = await c.req.json<{ item: string; quantity: number }>();
  const order = { id: nextId++, item: input.item, quantity: input.quantity, total: prices.get(input.item)! * input.quantity };
  orders.set(order.id, order);
  return c.json(order, 200);
});

app.get("/orders/:id", (c) => {
  const order = orders.get(Number(c.req.param("id")));
  return order ? c.json(order, 200) : c.body(null, 404);
});

const prices = new Map<string, number>([["Latte", 220], ["Masala Chai", 120]]);
const orders = new Map<number, { id: number; item: string; quantity: number; total: number }>();
let nextId = 103;

const app = new Hono();

// The contract says POST /orders returns 201 Created
// with a Location header like /orders/103.
// Set Location, then answer 201.
app.post("/orders", async (c) => {
  const input = await c.req.json<{ item: string; quantity: number }>();
  const order = { id: nextId++, item: input.item, quantity: input.quantity, total: prices.get(input.item)! * input.quantity };
  orders.set(order.id, order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

app.get("/orders/:id", (c) => {
  const order = orders.get(Number(c.req.param("id")));
  return order ? c.json(order, 200) : c.body(null, 404);
});

Content-Type: application/json

{"item":"Latte","quantity":2}

###
GET /orders/103` }
  }
];
