Five parts. 300+ commits. 43 alias mocks eliminated. One mission: tests that pass every time.
You've made it through the chronicles. The Reckoning taught you why tests fail randomly. Mock Madness showed you the alias mock trap. The Determinism Principle gave you control over time and randomness. The Teardown Tango mastered cleanup. The Abstraction Avalanche tamed your TestCase architecture.
Now you need a reference. Something to pull up during code review. Something to hand to a new team member. Something to feed your AI assistant.
This is that reference.
The Ultimate Checklist
Everything from Parts 1-5, organized by category.
Time & Randomness
Freeze time at test start with
Carbon::setTestNow()ortravelTo()Use
travelBack()aftertravelTo(), or use the callback formUse
startOfSecond()for back-to-back records to eliminate millisecond differencescreateFromFormat()uses current time for missing parts - chainstartOfDay()for date-onlyNever compare timestamps with
assertEquals- useassertEqualsWithDeltaor custom assertionsSeed random generators with fixed values - use
DataProvidersinstead of random valuesDataProvidersrun BEFORE Laravel boots - never usenow(),config(), or facades in themNever rely on database ordering without explicit
ORDER BYShuffle test data deliberately to expose hidden dependencies
Mocking
Never use Mockery
aliasoroverloadmocks - use dependency injection insteadUse
MockeryPHPUnitIntegrationtrait in baseTestCasePrefer Laravel fakes (
Http::fake,Queue::fake) over manual mocksHttp::fake()precedence matters - specific routes first, wildcards lastAvoid
Http::sequence()in parallel tests - responses are consumed unpredictablyFake BEFORE factory creation to catch
afterCreatinghooksUse partial fakes when possible -
Bus::fake([SpecificJob::class]),Event::fake([SpecificEvent::class])
Cleanup & Isolation
Use
DatabaseTransactionsfor test isolationCustom cleanup BEFORE
parent::tearDown()- Mockery and transactions close in parentClear caches BEFORE mocking, not after
Disable observable events in
setUpfor models with side effectsUse opt-in traits for targeted
tearDown(RefreshesSushiModels,CleansUpTestFiles)Prefer
Storage::fake()over manual file cleanupReset global state (config, locale) in
tearDownor at test end
Test Structure
Keep
TestCaselean - only global concernsUse traits for optional concerns with
setUpXXX/tearDownXXXnamingCall Passport clients lazily, not in global
setUpAlways specify scopes in
Passport::actingAs()Enable risky test detection in PHPUnit
Assertions
Use
assertSame(true, ...)notassertTrue()to catch missing boolean model castsCreate custom assertions for value objects (Money, dates)
For Money: use
Money->isEqualTo(), notassertEqualson floatsUse
$this->fail()in try/catch blocks to prevent risky testsPrefer
expectException()over try/catch when only asserting exception typeAssert exception class AND message when relevant
Verification
Run suspected flaky tests many times:
--repeat 100Run in random order:
--order-by=randomRun in parallel to expose race conditions
A test that fails once in 100 runs is a bug, not bad luck
The AI Agent Prompt
Why This Prompt?
AI coding assistants are increasingly writing and reviewing our tests. But they lack the hard-won knowledge of flaky test patterns - the alias mock trap, the DataProvider timing issue, the cache-before-mock ritual.
This prompt transfers that knowledge. Add it to your AI context and every test review catches the same issues a senior developer would.
Use it when:
AI writes tests for you - catch flakiness before commit
Debugging flaky failures - get pattern-based suggestions
Code review - automated checklist for test reliability
Onboarding - teach new devs through AI explanations
The Prompt
## Flaky Test Review Guidelines
When reviewing or debugging Laravel/PHP tests, check for these common flakiness patterns:
### Time-Related Issues
- [ ] Is time frozen with Carbon::setTestNow() or travelTo() at test start?
- [ ] Using travelBack() after travelTo(), or the callback form?
- [ ] Using startOfSecond() for back-to-back created records?
- [ ] createFromFormat() chains startOfDay() for date-only comparisons?
- [ ] DataProviders avoid now(), config(), facades? (They run before Laravel boots)
### Mock Issues
- [ ] Any Mockery alias:: or overload:: usage? (Use DI/Facades instead - these break parallel tests)
- [ ] Is MockeryPHPUnitIntegration trait used in base TestCase?
- [ ] Are fakes called BEFORE factory creation? (afterCreating hooks run immediately)
- [ ] Http::fake() has specific routes before wildcards?
- [ ] Avoiding Http::sequence() in parallel tests?
- [ ] Using partial fakes where possible? Bus::fake([Job::class]), Event::fake([Event::class])
### Isolation Issues
- [ ] Does test depend on database ordering without ORDER BY?
- [ ] Are caches cleared BEFORE mocking cached values?
- [ ] Are observable events disabled for models that trigger side effects?
- [ ] Is static state (singletons, static properties, Sushi connections) properly reset?
- [ ] Is global state (config, locale) reset in tearDown?
### tearDown Issues
- [ ] Custom cleanup runs BEFORE parent::tearDown()? (Mockery closes in parent)
- [ ] Are file handles closed before cleanup?
- [ ] Are opt-in cleanup traits used for specific concerns?
### Assertion Issues
- [ ] Using assertSame(true, ...) not assertTrue() for booleans? (Catches missing casts)
- [ ] Using Money->isEqualTo() not assertEquals for money values?
- [ ] Using $this->fail() in try/catch blocks? (Prevents risky tests)
### Common Fixes
1. Freeze time: `$this->travelTo(now()->startOfSecond())`
2. Disable events: `Model::ignoreObservableEvents(['created', 'updated'])`
3. Fake before factory: `Notification::fake(); $model = Model::factory()->create();`
4. Clear cache before mock: `cache()->forget('key'); Config::partialMock()`
5. Use Storage::fake() instead of manual file operations
6. Replace alias mocks with Factory Pattern or FacadesCopy this into your project as a TESTING_GUIDELINES.md or add it to your AI assistant context.
The complete series:
The Reckoning - Why tests fail randomly
Mock Madness - The alias mock trap
The Determinism Principle - Time and randomness
The Teardown Tango - Cleanup and assertions
The Abstraction Avalanche -
TestCasearchitecture
The End
300+ commits. 43 alias mocks eliminated. Countless flaky tests fixed.
And a TestCase that evolved from “throw everything in setUp” to “make dependencies explicit.”
The test that passes locally and fails in CI? That's not a flaky test. That's a bug in your foundation.
Build it right. Keep it deterministic. Trust your tests.
The Flaky Test Chronicles ends here. Now go forth and make your tests boring. Boring tests are good tests.