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. The closure receives $this bound to the spec's Subject.
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 in the current context, 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 set up, before any examples execute. Useful 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 both hooks', function () { // $this->log is ['outer', 'inner'] expect($this->log)->toBe(['outer', 'inner']); }); }); });
this(string $name, Closure $fn)
While not strictly a hook, this() is the primary way to set up shared state. The closure runs the first time an example reads the value, once per example, so every example gets its own value and the hooks have run by then: a beforeEach can arrange a double the this() consumes, and a this() may read one declared after it. A this() nobody reads never runs; one that throws fails that example alone. To build one before the example starts, read it in a beforeEach, as in beforeEach(fn () => $this->server); it is still built once for that example:
describe(UserService::class, function () { this('repo', fn () => mock(UserRepository::class)); this('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']); }); });
Type-Hinted Mock Injection
Both it() and this() closures support type-hinted parameters that are automatically resolved as mocks:
describe(Notifier::class, function () { // Mock injected via this() this('notifier', fn (Mailer $mailer) => new Notifier($mailer)); // Mock injected via it() it('sends email', function (Mailer $mailer) { expect($mailer)->toBeAnInstanceOf(Mailer::class); }); });
Any parameter with a class or interface type hint that can be mocked will be resolved automatically via mock().
Hook Execution Order
For a given example, hooks execute in this order:
beforeAll(once per context, on first run)beforeEach(parent contexts first, then current)- Example body
afterEach(current context first, then parent)afterAll(once per context, after last example)
Pending Contexts and Hooks
When a context is marked pending (xdescribe, xcontext), beforeAll and afterAll hooks are skipped entirely. Individual pending examples (xit, pending()) still trigger beforeEach/afterEach on the context but the example body is not executed.
Story BDD Hooks
Feature files have their own lifecycle hooks, registered in step files:
beforeFeature/afterFeature, beforeScenario/afterScenario and
beforeStep/afterStep. See Story BDD for details.