Typorithm · medium

2. Parameterized unit cases and boundary evidence

qatestingplaywrightindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

2. Parameterized unit cases and boundary evidence

0%

Concept and contract

Turn a small behavior table into readable regression tests.

Parameterization removes repeated test structure while keeping distinct inputs and expectations visible. Start from a manually reviewed case table. Generating many cases from the same predicate used by the implementation can make the suite large without giving it an independent oracle.

Give cases useful names so a failure identifies the input and intended rule. A test named invalid does less diagnostic work than one named rejects the integer below the minimum. Keep data small enough that a beginner can inspect every row.

Mechanism and worked example

The example checks the five nearby integer values for an inclusive one-to-three range.

The example checks the five nearby integer values for an inclusive one-to-three range. Each expected boolean is literal fixture data. The loop produces separate test names, so a changed comparison points to the affected boundary rather than one opaque aggregate failure.

Add separate primitive and fractional cases when they test different rules. Boundary neighborhoods alone do not establish that a numeric validator rejects a string. Avoid expanding the table mechanically when an additional row adds no new behavior.

Complete worked example

typescript

import { test, expect } from "../support/test.js";
import { roles, selectRoles, validInteger } from "../support/domain.mjs";
for (const [value, valid] of [[0,false],[1,true],[2,true],[3,true],[4,false]] as const) {
  test(`limit ${value} is ${valid}`, () => {
    expect(validInteger(value)).toBe(valid);
  });
}
Reasoning
Literal expected booleans keep the table independent and failures specific.

Counterexamples and common mistakes

The weak test calls the validator again to compute expected validity.

The weak test calls the validator again to compute expected validity. If both calls contain the same faulty comparison, the test passes. Reusing the implementation as its own oracle is the same problem whether it happens in a loop or in one assertion.

Do not mutate a shared expected table inside tests. Test order and parallelism can otherwise change the starting conditions. Readonly fixture data and local result variables make the dependency easier to understand.

Counterexample: what this check misses

typescript

import { test, expect } from "../support/test.js";
import { roles, selectRoles, validInteger } from "../support/domain.mjs";
test("weak generated oracle", () => {
  expect(validInteger(1)).toBe(validInteger(1));
});
Reasoning
Calling the same validator for the expected value reproduces its possible defect.

Independent practice: small task

Parameterize a zero-to-fifty integer rule with zero, fifty, minus one, fifty-one and a fraction.

Parameterize a zero-to-fifty integer rule with zero, fifty, minus one, fifty-one and a fraction. Begin with zero and the negative case, then add the remaining boundaries. Each row should explain one accepted or rejected behavior.

Use a concise case name containing the input. Optional extension: add a wrong-primitive case after explaining why it is a separate partition. The core exercise is a handful of readable tests, not a random-input framework.

Step 1: Write zero and minus one with literal expectations. Step 2: Add the upper bound and fractional case.

Practice starter: solve before reading the solution

typescript

import { test, expect } from "../support/test.js";
import { roles, selectRoles, validInteger } from "../support/domain.mjs";
// Add literal expected outcomes for the five chosen values.
test("experience boundary", () => {
  // TODO: expect(validInteger(value, 0, 50)).toBe(expected);
});
Reasoning
Begin with valid zero and an invalid negative value.
  • Hint 1: Zero is accepted by this contract.
  • Hint 2: Do not mutate the shared case table.

Solution, tests and trade-offs

The solution supplies explicit expected booleans and passes the chosen bounds to the validator.

The solution supplies explicit expected booleans and passes the chosen bounds to the validator. Zero remains valid. A mutation making the lower comparison exclusive is rejected by the zero case, which demonstrates the purpose of that boundary.

Run the suite with the correct implementation and the controlled wrong comparison separately. Record both outcomes. A successful parameterized loop is useful only when its data and oracle remain reviewable.

Practice solution and acceptance checks

typescript

import { test, expect } from "../support/test.js";
import { roles, selectRoles, validInteger } from "../support/domain.mjs";
for (const [value, valid] of [[0,true],[50,true],[-1,false],[51,false],[1.5,false]] as const) {
  test(`experience ${value} is ${valid}`, () => {
    expect(validInteger(value, 0, 50)).toBe(valid);
  });
}
Reasoning
The table covers inclusive bounds and integrality without hiding the expected decisions.

Interview reasoning and next step

In interviews, derive two boundary rows and explain which fault each rejects.

In interviews, derive two boundary rows and explain which fault each rejects. Discuss naming and fixture ownership before adding abstraction. Parameterization should reduce noise without hiding the important differences between cases.

Next, isolate external dependencies with fakes and mocks while preserving at least one test of the real integration boundary.

Optional stretch — after the core task works: Add a string input as a distinct partition.

Computer practice: download the companion lab at https://applydjinn.com/typorithm/qa-lab-20261105.zip. Read its README, install the pinned tools and run the accepted examples before attempting a starter. The mobile reader preserves your reading and self-check progress; the local lab runs browser tests separately.

Turn a small behavior table into readable regression tests.