Lifecycle Hooks

Hooks run setup and teardown code around examples, giving you control over shared state and cleanup.

beforeEach(Closure $fn)

Runs before every example in the current context:

describe(Calculator::class, function () {
    beforeEach(function () {
        $this->calculator = new Calculator();
    });

    it('adds', function () {
        expect($this->calculator->add(2, 3))->toBe(5);
    });

    it('subtracts', function () {
        expect($this->calculator->subtract(5, 3))->toBe(2);
    });
});

afterEach(Closure $fn)

Runs after every example, regardless of pass or fail:

describe('Database', function () {
    afterEach(function () {
        $this->db->rollback();
    });

    it('inserts a record', function () {
        // ...
    });
});

beforeAll(Closure $fn)

Runs once when the context is first entered, before any examples execute. Use for expensive setup that can be shared:

describe('ExpensiveService', function () {
    beforeAll(function () {
        $this->service = ExpensiveService::boot();
    });

    it('is ready', function () {
        expect($this->service->isReady())->toBeTrue();
    });
});

afterAll(Closure $fn)

Runs once after all examples in the context have finished:

describe('TempFiles', function () {
    afterAll(function () {
        // cleanup temp directory
    });
});

Nested hook inheritance

Hooks in parent contexts run for all nested examples. Inner hooks run after outer hooks:

describe('Outer', function () {
    beforeEach(function () {
        $this->log[] = 'outer';
    });

    context('Inner', function () {
        beforeEach(function () {
            $this->log[] = 'inner';
        });

        it('runs outer then inner hooks', function () {
            expect($this->log)->toContain('outer');
            expect($this->log)->toContain('inner');
        });
    });
});

Execution order

For a given example, hooks execute in this order:

OrderHookScope
1beforeAllOnce per context, on first run
2beforeEachParent contexts first, then current
3Example body
4afterEachCurrent context first, then parent
5afterAllOnce per context, after last example

let() as setup

While not strictly a hook, let() is the primary way to set up shared state. Type-hinted parameters auto-inject mocks:

describe(UserService::class, function () {
    let('repo', fn() => mock(UserRepository::class));
    let('service', fn() => new UserService($this->repo));

    it('finds users', function () {
        allow($this->repo->find(1))->toReturn(['name' => 'Alice']);
        expect($this->service->find(1))
            ->toBe(['name' => 'Alice']);
    });
});