Skip to content

Fakeable Behaviors

All EventMachine behaviors include the Fakeable trait, allowing you to mock and test them in isolation.

Basic Faking

Create a Fake

php
use App\Machines\Actions\ProcessOrderAction;

ProcessOrderAction::fake();

Check if Faked

php
ProcessOrderAction::isFaked(); // true

Get the Fake

php
$fake = ProcessOrderAction::getFake(); // Mockery mock instance

Setting Expectations

Basic Expectations

php
ProcessOrderAction::fake();

// Expect to run once
ProcessOrderAction::shouldRun()->once();

// Expect to run twice
ProcessOrderAction::shouldRun()->twice();

// Expect to run any number of times
ProcessOrderAction::shouldRun()->zeroOrMoreTimes();

// Expect never to run
ProcessOrderAction::shouldRun()->never();

With Arguments

php
ProcessOrderAction::fake();

ProcessOrderAction::shouldRun()
    ->once()
    ->withArgs(function (ContextManager $context) {
        return $context->orderId === 'order-123';
    });

With Return Values

php
ProcessOrderAction::fake();

// Return nothing (void)
ProcessOrderAction::shouldRun()->once();

// Execute custom logic
ProcessOrderAction::shouldRun()
    ->once()
    ->andReturnUsing(function (ContextManager $context) {
        $context->processed = true;
        $context->processedAt = now();
    });

Assertions

Assert Ran

php
ProcessOrderAction::fake();

// Run machine
$machine = OrderMachine::create();
$machine->send(['type' => 'PROCESS']);

// Assert the action ran
ProcessOrderAction::assertRan();

Assert Not Ran

php
ProcessOrderAction::fake();

// Don't trigger the action
$machine = OrderMachine::create();

// Assert the action did not run
ProcessOrderAction::assertNotRan();

Resetting Fakes

Reset Single Behavior

php
ProcessOrderAction::resetFakes();

Reset All Fakes

php
use Tarfinlabs\EventMachine\Facades\EventMachine;

EventMachine::resetAllFakes();

In Test Teardown

php
afterEach(function () {
    EventMachine::resetAllFakes();
});

Complete Example

php
use App\Machines\OrderMachine;
use App\Machines\Actions\ProcessOrderAction;
use App\Machines\Actions\SendNotificationAction;
use App\Machines\Guards\ValidateOrderGuard;
use Tarfinlabs\EventMachine\Facades\EventMachine;

beforeEach(function () {
    // Create fakes
    ProcessOrderAction::fake();
    SendNotificationAction::fake();
    ValidateOrderGuard::fake();
});

afterEach(function () {
    EventMachine::resetAllFakes();
});

it('processes order when valid', function () {
    // Setup guard to pass
    ValidateOrderGuard::shouldRun()
        ->once()
        ->andReturn(true);

    // Setup actions
    ProcessOrderAction::shouldRun()
        ->once()
        ->andReturnUsing(function ($context) {
            $context->orderId = 'order-123';
            $context->processed = true;
        });

    SendNotificationAction::shouldRun()->once();

    // Execute
    $machine = OrderMachine::create();
    $machine->send(['type' => 'SUBMIT']);

    // Assert
    expect($machine->state->matches('processing'))->toBeTrue();
    expect($machine->state->context->orderId)->toBe('order-123');

    ProcessOrderAction::assertRan();
    SendNotificationAction::assertRan();
});

it('rejects order when validation fails', function () {
    // Setup guard to fail
    ValidateOrderGuard::shouldRun()
        ->once()
        ->andReturn(false);

    // Execute
    $machine = OrderMachine::create();
    $machine->send(['type' => 'SUBMIT']);

    // Assert - stays in pending (guard blocked transition)
    expect($machine->state->matches('pending'))->toBeTrue();

    // Action should not have run
    ProcessOrderAction::assertNotRan();
});

Faking Guards

php
use App\Machines\Guards\HasPermissionGuard;

HasPermissionGuard::fake();

// Always pass
HasPermissionGuard::shouldRun()
    ->andReturn(true);

// Always fail
HasPermissionGuard::shouldRun()
    ->andReturn(false);

// Conditional
HasPermissionGuard::shouldRun()
    ->andReturnUsing(function ($context) {
        return $context->userId === 'admin';
    });

Faking Validation Guards

php
use App\Machines\Guards\ValidateAmountGuard;

ValidateAmountGuard::fake();

// Pass validation
ValidateAmountGuard::shouldRun()->andReturn(true);

// Fail validation (exception will be thrown)
ValidateAmountGuard::shouldRun()->andReturn(false);

Faking Calculators

php
use App\Machines\Calculators\CalculateTotalCalculator;

CalculateTotalCalculator::fake();

CalculateTotalCalculator::shouldRun()
    ->once()
    ->andReturnUsing(function ($context) {
        $context->subtotal = 100;
        $context->tax = 10;
        $context->total = 110;
    });

Testing with Dependencies

When behaviors have constructor dependencies, faking bypasses them:

php
class ProcessOrderAction extends ActionBehavior
{
    public function __construct(
        private readonly PaymentGateway $gateway, // Not called when faked
    ) {}
}

// No need to mock PaymentGateway
ProcessOrderAction::fake();
ProcessOrderAction::shouldRun()->once();

Multiple Calls

php
ProcessOrderAction::fake();

// Expect specific number of calls
ProcessOrderAction::shouldRun()->times(3);

// Different behavior per call
ProcessOrderAction::shouldRun()
    ->once()
    ->andReturnUsing(fn($ctx) => $ctx->step = 1);

ProcessOrderAction::shouldRun()
    ->once()
    ->andReturnUsing(fn($ctx) => $ctx->step = 2);

Testing Raised Events

php
class ProcessAction extends ActionBehavior
{
    public function __invoke(ContextManager $context): void
    {
        $context->processed = true;
        $this->raise(['type' => 'PROCESSED']);
    }
}

// Fake but preserve raise behavior
ProcessAction::fake();
ProcessAction::shouldRun()
    ->once()
    ->andReturnUsing(function ($context) use ($action) {
        $context->processed = true;
        // Can't easily test raise() with fakes
        // Consider integration test instead
    });

Integration vs Unit Testing

Unit Testing (with Fakes)

php
// Fast, isolated
ProcessOrderAction::fake();
ValidateOrderGuard::fake();

$machine = OrderMachine::create();
$machine->send(['type' => 'SUBMIT']);

ProcessOrderAction::assertRan();

Integration Testing (without Fakes)

php
// Slower, tests real behavior
$machine = OrderMachine::create();
$machine->send(['type' => 'SUBMIT']);

expect($machine->state->context->orderId)->not->toBeNull();
$this->assertDatabaseHas('orders', ['id' => $machine->state->context->orderId]);

Best Practices

1. Reset Fakes Between Tests

php
afterEach(function () {
    EventMachine::resetAllFakes();
});

2. Be Explicit About Expectations

php
// Good - explicit expectation
ProcessAction::shouldRun()->once();

// Avoid - no expectation
ProcessAction::fake();

3. Test Both Success and Failure Paths

php
it('processes when valid', function () {
    ValidateGuard::fake()->shouldRun()->andReturn(true);
    // ...
});

it('rejects when invalid', function () {
    ValidateGuard::fake()->shouldRun()->andReturn(false);
    // ...
});

4. Use Integration Tests for Complex Flows

For complex multi-step flows, consider integration tests without fakes.

Released under the MIT License.