Testing Overview
EventMachine is designed for testability at every level — from isolated unit tests of individual behaviors to full machine-level acceptance tests.
Philosophy
- Layered testing pyramid: behavior → transition → path
- Real by default, opt-in faking: behaviors run with real logic unless you explicitly fake them
- Container-first architecture: all behaviors are resolved via
App::make(), enabling constructor DI and mockability
Quick Start
Three test levels, one behavior:
// 1. Isolated — unit test a single guard
$state = State::forTesting(['count' => 4]);
expect(IsEvenGuard::runWithState($state))->toBeTrue();
// 2. Faked — mock a behavior during machine execution
SendEmailAction::shouldRun()->once();
OrderMachine::test()->send('SUBMIT')->assertBehaviorRan(SendEmailAction::class);
// 3. Fluent — full path test with TestMachine
TrafficLightsMachine::test()
->assertState('active')
->send('INCREASE')
->assertContext('count', 1);
// 4. Inline faking — fake inline closures during machine execution
OrderMachine::test()
->faking(['broadcastAction', 'isValidGuard' => true])
->send('SUBMIT')
->assertBehaviorRan('broadcastAction');Test Setup
Pest / PHPUnit Configuration
Use the InteractsWithMachines trait to automatically reset all fakes between tests. This prevents state leaking across test cases — no manual resetMachineFakes() or resetAllFakes() needed.
// tests/Pest.php
uses(
Tests\TestCase::class,
Illuminate\Foundation\Testing\RefreshDatabase::class,
Tarfinlabs\EventMachine\Testing\InteractsWithMachines::class,
)->in('Feature');Automatic Cleanup
InteractsWithMachines automatically resets all Machine::fake(), CommunicationRecorder, and InlineBehaviorFake state after each test. No manual resetMachineFakes() needed.
Prerequisite
InteractsWithMachines requires your TestCase to extend Laravel's or Orchestra Testbench's TestCase. Pure PHPUnit TestCase without InteractsWithTestCaseLifecycle won't auto-call the teardown.
In-Memory Database
For fast tests, use SQLite in-memory. This eliminates migration overhead and disk I/O — each test gets a fresh database without touching the filesystem. Combined with RefreshDatabase, tests run in complete isolation.
<!-- phpunit.xml -->
<php>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
</php>Testing Layers
| Layer | What to Test | Guide |
|---|---|---|
| Event Building | Complex test event payloads with faker, DB seeding | Isolated Testing — EventBuilder |
| Behavior (Unit) | Individual guards, actions, calculators | Isolated Testing |
| Faking | Mock behaviors during execution | Fakeable Behaviors |
| Inline Faking | Fake inline closures (actions, guards, calculators) | Fakeable Behaviors — Inline |
| Constructor DI | Service injection + mocking | Constructor DI |
| Transition (Integration) | Guard pass/fail, state changes, paths | Transitions & Paths |
| Machine (Acceptance) | Full fluent test wrapper | TestMachine |
| Parallel | Dispatch verification, region isolation | Parallel Testing |
| Inter-Machine | Child machine faking, sendTo/dispatchTo assertions | Inter-Machine Testing |
| Job Actors | Job dispatch verification, fire-and-forget | Inter-Machine Testing — Job Actors |
| Fire-and-Forget | Machine + queue (no @done) delegation testing | Inter-Machine Testing — Fire-and-Forget |
| Available Events | Which events current state accepts, forward event availability | TestMachine — Available Events |
| Scheduled Events | Cron-based batch operations via runSchedule(), assertHasSchedule() | Scheduled Testing |
| Time-Based | advanceTimers(), assertHasTimer(), timer lifecycle testing | Time-Based Testing |
| Persistence | DB, restoration, archival | Persistence Testing |
| Path Coverage | Enumerate all paths, track coverage, assert thresholds | Transitions & Paths — Coverage |
| Recipes | Common real-world patterns | Recipes |
| Migration | Upgrading from legacy test patterns | Migration Patterns |
How to Choose Your Testing Approach
What are you testing?
| Goal | Tool | Guide |
|---|---|---|
| Single guard/action in isolation | State::forTesting() + runWithState() | Isolated Testing |
| Raised event from action? | assertRaised() after runWithState() | Isolated Testing — Raised Events |
| Complex event payload | Event::builder()->withX()->make() | Isolated Testing — EventBuilder |
| Full machine flow | Machine::test() | TestMachine |
| Deep state without path replay | Machine::startingAt() | TestMachine — Starting At |
| Child machine delegation | fakingChild() / simulateChildDone() | Delegation Testing |
| Job actor completion | simulateChildDone(MyJob::class) | Delegation Testing — Job Actors |
| Bulk fake all behaviors | fakingAllActions(except: [...]) | TestMachine — Bulk Faking |
| Timer behavior | advanceTimers() | Time-Based Testing |
| Parallel states | assertRegionState() | Parallel Testing |
| Forward endpoints | withRunningChild() | Delegation Testing — Forward |
| Cross-machine messaging | recordingCommunication() | Delegation Testing — sendTo |
| Real async pipeline | LocalQA with Horizon | Real Infrastructure |
| Something not working? | — | Troubleshooting |
Testing Strategy
For guidance on choosing the right test level and avoiding common traps, see Testing Strategy.
When Fakes Aren't Enough
Most EventMachine testing works fine with fakes. But some scenarios can only be verified with real infrastructure:
| What fakes verify | What fakes DON'T verify |
|---|---|
Job dispatch (Queue::assertPushed) | Job execution → child runs → completion routes back |
Timer registration (assertHasTimer) | machine:process-timers command fires correctly |
Schedule definition (assertHasSchedule) | machine:process-scheduled runs via scheduler |
Child invocation (assertChildInvoked) | Real child starts, persists, reaches final state |
| Lock exception thrown | Concurrent requests actually block each other |
Rule of thumb: If you check "was the right thing dispatched/registered?" → fakes are sufficient. If you need "does the full pipeline complete end-to-end?" → real infrastructure.
When to invest:
- Async delegation (
queue) where parent behavior after@doneis business-critical - Timers (
after/every) that must be verified against the sweep command - Concurrent access with real database locks
- Forward endpoints with full HTTP → child → response chain
See Recipes for real infrastructure testing patterns.