Mid-Level Automation · Practice 05

Write a Data-Driven Test from a Spec Sheet

The business has provided a tax bracket spec sheet for NZ PAYE calculations. Your job: write a parameterised Playwright API test that verifies all brackets — including the boundary values where mistakes are most likely to hide.

1 The Scenario

Data-Driven API Testing

System under test: IRD PAYE calculation API — the same engine used by NZ payroll software to calculate income tax withholding

Endpoint: POST /api/tax/calculate

Your job: The business analyst has handed you a spec sheet with five tax brackets. You need to write one test definition that runs against every bracket — including the transition points where bugs are most likely to appear. Manual testing this table would take 30 minutes. Automated, it takes 3 seconds.

2 The Spec Sheet

This is the NZ PAYE income tax table for the 2026 tax year. Each row is a test requirement. Your job is to turn rows into test cases — especially the boundary values at the top and bottom of each bracket.

Annual Income Marginal Rate Tax Calculation
$0 – $14,000 10.5% Income × 10.5%
$14,001 – $48,000 17.5% $1,470 + 17.5% on excess over $14,000
$48,001 – $70,000 30% $8,450 + 30% on excess over $48,000
$70,001 – $180,000 33% $15,050 + 33% on excess over $70,000
$180,001+ 39% $51,380 + 39% on excess over $180,000
Why boundary values matter: The transition points between brackets are the highest-risk values. An off-by-one error in the bracket logic applies the wrong rate to every affected taxpayer. Always test $14,000, $14,001, $48,000, $48,001 — the exact edges.

3 API Contract

The API takes a JSON body and returns the calculated tax. These are the shapes you'll work with.

Request — POST /api/tax/calculate
{
  "annual_income": 50000,
  "tax_year": "2026"
}
Response — 200 OK
{
  "annual_income": 50000,
  "annual_tax": 9620,
  "effective_rate": 19.24
}

The effective_rate is the blended rate across all applicable brackets, not the marginal rate. Assert on annual_tax as the primary correctness check. Use a tolerance of ±0.01 to allow for floating point rounding.

4 Your Task

Write the parameterised test

Write a Playwright API test that covers all five brackets with at least two test cases each — one mid-bracket and one at a boundary. Requirements:

  • Use a for loop or test.each — do not copy the test body five times
  • Include at least one test case at each bracket boundary (the transition point)
  • Assert on HTTP status, annual_income echo, and annual_tax value
  • Use a ±0.01 tolerance for the tax amount (floating point rounding)
  • Name each test case so the test output tells you exactly which case failed

Hint: Build a test case array. Each object has income, expected_tax, and desc. Loop over it.

Model Answer

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

// Each row in the spec table becomes test cases here.
// Include mid-bracket values AND boundary values.
const TAX_TEST_CASES = [
  // First bracket: 10.5%
  { income: 0,      expected_tax: 0,       desc: 'zero income' },
  { income: 7000,   expected_tax: 735,     desc: 'first bracket mid-point' },
  { income: 14000,  expected_tax: 1470,    desc: 'first bracket upper boundary' },

  // Second bracket: 17.5% on excess over $14,000
  { income: 14001,  expected_tax: 1470.175, desc: 'second bracket lower boundary' },
  { income: 30000,  expected_tax: 4270,    desc: 'second bracket mid-point' },
  { income: 48000,  expected_tax: 8450,    desc: 'second bracket upper boundary' },

  // Third bracket: 30% on excess over $48,000
  { income: 48001,  expected_tax: 8450.30, desc: 'third bracket lower boundary' },
  { income: 60000,  expected_tax: 12050,   desc: 'third bracket mid-point' },
  { income: 70000,  expected_tax: 15050,   desc: 'third bracket upper boundary' },

  // Fourth bracket: 33% on excess over $70,000
  { income: 70001,  expected_tax: 15050.33, desc: 'fourth bracket lower boundary' },
  { income: 120000, expected_tax: 31550,   desc: 'fourth bracket mid-point' },
  { income: 180000, expected_tax: 51380,   desc: 'fourth bracket upper boundary' },

  // Fifth bracket: 39% on excess over $180,000
  { income: 180001, expected_tax: 51380.39, desc: 'top rate lower boundary' },
  { income: 250000, expected_tax: 78680,   desc: 'high income' },
];

for (const { income, expected_tax, desc } of TAX_TEST_CASES) {
  test(`PAYE calculation: ${desc} (income $${income.toLocaleString('en-NZ')})`, async ({ request }) => {
    const response = await request.post('/api/tax/calculate', {
      data: { annual_income: income, tax_year: '2026' }
    });

    expect(response.status()).toBe(200);

    const body = await response.json();

    // API should echo back the income we sent
    expect(body.annual_income).toBe(income);

    // Allow ±0.01 for floating point rounding
    expect(Math.abs(body.annual_tax - expected_tax)).toBeLessThan(0.01);
  });
}
Key principle: Data-driven testing multiplies your coverage without multiplying your code. You write the assertion logic once — the test runner handles the loop. When a new bracket is added, you add one row to the array. When a boundary changes, you update one number. The test count scales with the data, not the code.

When a test in this suite fails, the output tells you exactly which case broke — e.g. PAYE calculation: first bracket upper boundary (income $14,000) — so you go straight to the right row in the spec table. No guessing.

5 Go Further

Extend the suite

Once you have the basic suite passing, try these extensions:

  • Error cases: Add test cases for invalid inputs — negative income, missing tax_year, non-numeric income. Assert that the API returns 400 with a meaningful error message.
  • Effective rate check: Add an assertion on effective_rate. Calculate the expected effective rate from your test data (expected_tax / income * 100) and assert it matches within 0.01%.
  • Load the test cases from JSON: Move TAX_TEST_CASES into a fixtures/paye-brackets.json file and import it. This lets a business analyst update test data without touching TypeScript.