Skip to content

test: start applying the isolated testing style - #16

Merged
hughgrigg merged 3 commits into
mainfrom
test/hg/isolated-testing-style
Aug 17, 2026
Merged

test: start applying the isolated testing style#16
hughgrigg merged 3 commits into
mainfrom
test/hg/isolated-testing-style

Conversation

@hughgrigg

Copy link
Copy Markdown
Contributor

Applies the isolated-testing-style and part-factory-test-data skills to
Quando's test suite. Seven of the thirteen test files are done here, and the
remaining six are listed at the bottom.

The survey found less to fix than expected. No mocks, no spies, no
beforeEach/afterEach, and no call-count assertions outside one deliberate
case. What was missing was given/when/then, and what needed removing was the
module-level fixtures every file shared.

Shared fixtures out, local factories in

Before, one rota built at module scope and used by six tests:

const ON_CALL = rota()
  .assign(weekdays(), "alice")
  .assign(weekends(), "bob")
  .swap("2026-03-11", "carol");

it("says who is on", () => {
  assertIdentical(ON_CALL.whoIsOn(when("2026-03-09T10:00")), "alice");
  assertIdentical(ON_CALL.whoIsOn(when("2026-03-14T10:00")), "bob");
});

After, each test builds its own from a factory inside the describe, with names
from faker:

const anOnCallRota = () => {
  const [weekday = "", weekend = "", covering = ""] =
    faker.helpers.uniqueArray(() => faker.person.firstName(), 3);

  return {
    weekday,
    weekend,
    covering,
    onCall: rota()
      .assign(weekdays(), weekday)
      .assign(weekends(), weekend)
      .swap("2026-03-11", covering),
  };
};

it("says who is on", () => {
  // Given the rota.
  const { weekday, weekend, onCall } = anOnCallRota();

  // When it is asked about a Monday morning and a Saturday morning.
  // Then each falls to whoever holds that part of the week.
  assertIdentical(onCall.whoIsOn(when("2026-03-09T10:00")), weekday);
  assertIdentical(onCall.whoIsOn(when("2026-03-14T10:00")), weekend);
});

uniqueArray is there so two people are never the same person. The rendered
expectations interpolate the names as well, which is the part that earns the
change: a test comparing against a literal it also wrote proves less than one
checking the value it was handed comes back.

assertIdentical(
  renderValued(week),
  `[2026-03-09T00:00:00,2026-03-11T00:00:00)=${weekday} ` +
    `[2026-03-11T00:00:00,2026-03-12T00:00:00)=${covering} ` +
    `[2026-03-12T00:00:00,2026-03-14T00:00:00)=${weekday} ` +
    `[2026-03-14T00:00:00,2026-03-16T00:00:00)=${weekend}`,
);

Given, when, then

Filled in with the case, not with a restatement of the code. The half-open
boundary tests now carry the reason they exist:

it("excludes the end", () => {
  // Given the same day and the instant it closes.
  // When containment is tested.
  // Then closing time is outside. Two adjacent days would otherwise both
  // claim it.
  assertFalse(contains(workday, FIVE));
});

Three decisions worth reviewing

Dates stay fixed. They are the domain data under test. Randomising them
would mean computing the expected boundaries with the code under test, which the
skill rules out. Faker is used for values the assertions do not turn on: names, a
count, a take size.

One test keeps literal names. The type is what it asserts, and a faker name
widens to string:

const onCall = rota().assign(weekdays(), "alice").assign(weekends(), "bob");

const who: "alice" | "bob" | undefined = onCall.whoIsOn(MONDAY);

A lint rule had to give way. unicorn/consistent-function-scoping wants a
helper hoisted to module scope, which is where the shared fixtures this change
removes come from. It is off for test files now, with the reasoning in place:

// Support for a test belongs inside the describe it serves, where a
// reader meets it beside the tests that use it. This rule hoists it to
// module scope, which is where shared mutable fixtures come from.
// Rebuilding a closure per call costs nothing at this size.
"unicorn/consistent-function-scoping": "off"

Still to do

build, parse, query, resolve, interval-stream and interpret, in a
follow-up. One deliberate exception is planned for interval-stream: it asserts
pull counts through metered(), which reads as a call-count assertion. Laziness
is real behaviour and the pull count is the only way to observe it, so it stays,
with a comment saying why.

Checks

pnpm check passes, and passed at each commit on the branch.

docs/ satisfies the site scaffold's contract.
      Tests  200 passed (200)
Statements   : 99.49% ( 394/396 )
Branches     : 97.22% ( 245/252 )

Behaviour is untouched. Every one of the 200 tests asserts what it asserted
before, and @faker-js/faker is the one new devDependency.

First slice of the sweep: stream, time-rules and index.

Each test now opens with given/when/then filled in with the case rather
than a restatement of the code, the helper that was at module scope has
moved inside its describe, and take's count comes from faker so the test
proves it honours whatever it is given.

Dates stay fixed throughout. They are the domain data under test, and
randomising them would mean computing the expected boundaries with the
code under test.

Comments follow the prose rules too, so the significance tails and the
colon explainers in the old ones are gone.
cascade, rota and schedule. The shared ON_CALL and OPENING_HOURS fixtures
are gone: each test builds what it needs from a local factory, so no
test can lean on how another left things.

Rota names now come from faker, through uniqueArray so two people are
never the same person, and the rendered expectations interpolate them.
That proves the value is carried through rather than matching a literal
the test also wrote. One test keeps literal names on purpose, because
the type is what it asserts and a faker name widens to string.

One lint rule had to give way. unicorn/consistent-function-scoping wants
a helper hoisted to module scope, which is where shared mutable fixtures
come from and what the style exists to avoid. It is off for test files
now, with the reasoning in .oxlintrc.json.
The edge instants move inside a root describe, and each test says which
case it is about. The half-open cases carry their reason now: closing
time is outside the day so that two adjacent days cannot both claim it,
and the touching-versus-overlapping pair says what the gap between the
two comparisons buys a union.
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 52 minutes

Limit details: You’ve used all 2 included reviews currently available under your plan. You completed 83 included PR reviews in the past 7 days; at that activity level, included reviews refill at 2 reviews per hour.

This review is too large to run within your organization's remaining usage spending cap. Raise or remove your spending cap in the billing tab, then retry.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d9f35df5-05bd-4b50-9574-f83185806465

📥 Commits

Reviewing files that changed from the base of the PR and between f469852 and 115a549.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • .oxlintrc.json
  • package.json
  • src/cascade.test.ts
  • src/index.test.ts
  • src/interval.test.ts
  • src/rota.test.ts
  • src/schedule.test.ts
  • src/stream.test.ts
  • src/time-rules.test.ts

Comment @coderabbitai help to get the list of available commands.

@hughgrigg
hughgrigg merged commit 9b563cf into main Aug 17, 2026
6 checks passed
@hughgrigg
hughgrigg deleted the test/hg/isolated-testing-style branch August 17, 2026 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant