Skip to content

Testing with Constructor DI

Behaviors support constructor dependency injection. Services are resolved by the Laravel container, making them fully mockable in tests.

Class-based behaviors only

Constructor DI applies only to class-based behaviors resolved through App::make(). Inline closures bypass the container and do not receive constructor injection. For testing inline behaviors, see Inline Behavior Faking.

Two-Layer DI Architecture

EventMachine uses two separate injection layers. The constructor receives long-lived services (database repositories, API clients) resolved once by Laravel's container. The __invoke method receives per-transition state (context, event, history) injected by the engine. This separation makes behaviors easy to test: mock the services, provide test state.

LayerWhatWhereResolved By
ServicesPaymentGateway, Logger, Repository__construct()Laravel container via App::make()
StateContextManager, EventBehavior, State__invoke()injectInvokableBehaviorParameters

Mocking Injected Services

With runWithState() — Isolated

Use App::instance() or Mockery to replace the service in Laravel's container before calling runWithState(). The container resolves the mock just like production code would.

php
it('calls payment gateway with correct amount', function () {
    $this->mock(PaymentGateway::class)
        ->shouldReceive('charge')->with(100)->once()
        ->andReturn(new PaymentResult(id: 'txn_123'));

    $state = State::forTesting(['amount' => 100]);
    ProcessPaymentAction::runWithState($state);

    expect($state->context->get('transactionId'))->toBe('txn_123');
});

With Machine::test() — Integration

In machine-level tests, mock the service the same way — the container binding is global. The difference is that the full machine lifecycle runs, so you're testing the behavior within its real transition context.

php
it('processes payment in the full machine', function () {
    $this->mock(PaymentGateway::class)
        ->shouldReceive('charge')->andReturn(new PaymentResult(id: 'txn_456'));

    OrderMachine::test(['amount' => 100])
        ->send('PROCESS_PAYMENT')
        ->assertState('paid')
        ->assertContext('transactionId', 'txn_456');
});

Before/After Comparison

Previously, behaviors that needed external services used the service locator pattern (calling app() inside __invoke). Constructor DI is cleaner: dependencies are explicit, testable, and visible in the class signature.

Before — Service Locator (anti-pattern)

php
class ProcessPaymentAction extends ActionBehavior {
    public function __invoke(ContextManager $context): void {
        $gateway = app(PaymentGateway::class);  // hidden dependency
        $result = $gateway->charge($context->get('amount'));
    }
}

After — Constructor DI

php
class ProcessPaymentAction extends ActionBehavior {
    public function __construct(
        private readonly PaymentGateway $gateway,
        ?Collection $eventQueue = null,
    ) {
        parent::__construct($eventQueue);
    }

    public function __invoke(ContextManager $context): void {
        $result = $this->gateway->charge($context->get('amount'));  // explicit
    }
}

Decision Guide: Mock Service vs Mock Behavior

ApproachWhenExample
Mock the serviceTest behavior logic with controlled service responses$this->mock(PaymentGateway::class)
Mock the behaviorTest machine flow, skip behavior internalsProcessPaymentAction::fake()
Mock neitherE2E with real services (or test doubles in ServiceProvider)Full integration test

Related

See Isolated Testing for runWithState() details, Fakeable Behaviors for the faking API, TestMachine for the fluent machine-level wrapper, and Migration Patterns for upgrading from legacy test patterns.

For DI patterns beyond testing, see Dependency Injection.

Released under the MIT License.