Test suite assertion count is nondeterministic across runs #60
Labels
No labels
app
backlog
bug
ci-cd
contribution welcome
duplicate
enhancement
good first issue
help wanted
question
testing
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lvl0/dishplanner#60
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The test suite passes consistently (152 tests, 1 skipped) but the assertion count varies between runs on an unchanged working tree. Observed on
release/v0.9.0atcb2dbd9.Evidence
Three consecutive full-suite runs, no code changes between them:
Narrowed to the Schedule tests via
--filter=Schedule, which reproduces the same ±2 swing:Confirmed contributing cause: date-dependent assertion count
tests/Unit/Schedule/Services/ScheduleCalendarServiceTest.php::test_marks_today_correctlybuilds its calendar from the real current date:The loop asserts once per real day in the current month, minus today, so the count is a function of the calendar:
A suite whose assertion count depends on today's date is fragile: it makes assertion totals useless as a regression signal and means February silently exercises 3 fewer assertions than August.
Sibling tests in the same class avoid this by pinning the date (
getCalendarDays($planner, 3, 2026),..., 4, 2026), which is the pattern to follow — freeze time withCarbon::setTestNow()/travelTo(), or pin the month/year and assert a fixed count.Open question — a second source remains
Month length cannot change between two runs seconds apart, so date-dependence explains fragility across days, not the ±2 observed within one session. A filtered run of
ScheduleCalendarServiceTestalone gave a stable 162 assertions across the runs checked, so the remaining run-to-run variance was not isolated to that class.There is a second, genuinely nondeterministic source somewhere in the Schedule tests that has not been identified. Candidates not yet ruled out: faker-driven fixture counts feeding assertion loops, or ordering/collection-size variation in the schedule generation tests.
Suggested work
test_marks_today_correctly(and audit the class for other uses ofnow()).Notes
PASS 227 files) and PHPStan (clean) both green at the time of filing.Resolved
Fixed in
c3a5532. Both sources identified — including the "second source" the description left open.1. Date-dependent loop (drift across days)
ScheduleCalendarServiceTest::test_marks_today_correctlybuilt its calendar from the realnow()and asserted once per day in the current month. Fixed by freezing the clock to 2026-03-15 viatravelTo()and pinning the month/year, matching the sibling tests that already pass(planner, 3, 2026). The loop is now a singleassertCount(0, ...).travelBack()is deliberately not called:InteractsWithTestCaseLifecycle::tearDownTheTestEnvironment()(framework lines 155–161) unconditionally resetsCarbon::setTestNow()after every test, pass or fail, so the frozen clock cannot leak into siblings.2. Variable-length assertion loop (the ±2 within one session) — found
The open question is answered: it was
ScheduleGeneratorTest::test_it_takes_minimum_recurrences_into_account.It used
->reduce()to assert once per gap between placements of the recurring dish. How many times the generator places that dish depends on random fixture state, so the assertion count tracked a random outcome. Reproduced directly — the file alone gave 17 then 19 assertions on consecutive runs.This is only visible in aggregate. Run in isolation it sits stable at 9, because
RefreshDatabasereseeds identically when it runs first — which is why the original--filter=ScheduleCalendarServiceTestprobe in the description came back clean and pointed away from the real culprit.Fixed by collecting the gaps and making one fixed assertion over them, which also yields a better failure message than N independent assertions.
Coverage fix carried along
The filter changed from
scheduledUserDishes()->first()->userDish->dish->id === $dishRecurring->idto a->contains()over the whole relation.UserDishRepository::findInterferingUserDishes()controls which dishes enter the candidate pool but not the insert order ofScheduledUserDishrows within a schedule — that followsUser::all()iteration order. The old check silently skipped any schedule where the recurring dish landed on a user who wasn't first, so the test was under-counting. Confirmed in review against the repository source.Verification
--filter=ScheduleNote on
phpstan-baseline.neonRemoving the
reduce()left an unmatched ignore pattern, which fails the analyse gate. The stale entry is removed in the same commit — the baseline change is required, not cosmetic.Suggestion 3 from the description (a fixed clock for the whole suite) was not implemented. The two specific sources are fixed and the count is stable; a suite-wide clock is a larger change worth its own ticket if the class of drift reappears.