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
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 |
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"
}{
"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
forloop ortest.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_incomeecho, andannual_taxvalue - 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.tsimport { 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);
});
}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_CASESinto afixtures/paye-brackets.jsonfile and import it. This lets a business analyst update test data without touching TypeScript.