skip to content

In a Pest test closure, what is $this, how does beforeEach() share state through it, and what does pest()->extend()->in() change?

level: middleimportance: must knowfreq 50%

answer

  1. closure bound to a test case instance
  2. fresh instance per test
  3. generated class allows dynamic properties
  4. no $this in beforeAll()
  5. tests/Pest.php, extend, use, in

basics

~20 s

$this is the PHPUnit test case instance the closure is bound to, a fresh one per test. beforeEach() stores state on it, such as $this->cart. pest()->extend(Base::class)->in('Feature') makes tests in that folder bind to your base class instead.

solid answer

~40 s

Pest generates a PHPUnit test class per file and binds every test closure, and every `beforeEach()`/`afterEach()` closure, to the test case instance, so `$this` is that object - `PHPUnit\Framework\TestCase` unless configured. PHPUnit builds a new instance per test, so `beforeEach(function () { $this->cart = new Cart(); })` gives each test a fresh cart; the generated class carries `#[\AllowDynamicProperties]`, so those properties raise no PHP 8.2+ deprecation. `beforeAll()`/`afterAll()` run once per file with no instance, so `$this` is unavailable there. In `tests/Pest.php`, `pest()->extend(Tests\TestCase::class)->in('Feature')` binds tests under that folder to your base class, making its public and protected methods callable as `$this->...`; `->use(SomeTrait::class)` adds traits, and calling `pest()->extend()` in a test file without `in()` affects just that file. The older `uses()` function still works.

code

php · 5 lines
php
<?php
// tests/Pest.php
declare(strict_types=1);

pest()->extend(Tests\ShopTestCase::class)->in('Feature');

go deeper

for a junior

Recall that $this is the test case, that beforeEach() can store things like $this->cart, and that tests/Pest.php configures the base class.

for a middle

Explain closure binding to a fresh instance per test, why dynamic properties raise no deprecation, and why beforeAll() has no $this.

for a senior

Show how you organise base test cases and traits per folder with pest()->extend()->use()->in() so helpers stay typed and suites stay isolated.

for a principal

Decide how much shared behaviour lives in base classes versus plain helpers, balancing convenience against hidden coupling across the suite.

## Where `$this` comes from A Pest test is a closure, and closures in PHP can be **bound** to an object so that `$this` inside them refers to it. Pest relies on that. For each test file it generates a PHPUnit test class - a `final` class extending the configured base class, by default `PHPUnit\Framework\TestCase` - and turns each `test()`/`it()` into a method. When PHPUnit runs that method, Pest binds your closure to the current test case instance and calls it. So inside a test: ```php it('uses PHPUnit underneath', function () { $this->assertInstanceOf(PHPUnit\Framework\TestCase::class, $this); // passes }); ``` Two consequences follow: - Every PHPUnit assertion and helper on the base class is available as `$this->...`. - PHPUnit creates a **new instance for every test**, so anything stored on `$this` does not leak into the next test. ## Sharing state with `beforeEach()` `beforeEach()` closures are bound to the same instance and run before each test, after the base class's own `setUp()`: ```php beforeEach(function () { $this->cart = new Cart(); }); it('starts empty', function () { expect($this->cart->count())->toBe(0); }); ``` `$this->cart` is a **dynamic property**; PHP 8.2 deprecated creating those on classes that do not allow them. Pest's generated class is declared with `#[\AllowDynamicProperties]`, which is why this pattern produces no deprecation. `afterEach()` runs after each test with the same `$this`, for clean-up. `beforeAll()` and `afterAll()` are different: they run once per file, before the first and after the last test, when no test case instance exists. `$this` is not available there, so they suit process-wide preparation only. ## `pest()->extend()->in()` The configuration file `tests/Pest.php` is loaded automatically. Its main job is choosing the base class: ```php // tests/Pest.php pest()->extend(Tests\TestCase::class)->in('Feature'); ``` Now tests in `tests/Feature` are bound to an instance of `Tests\TestCase`. Any `public` or `protected` method you define there - a `makeCartWithItems()` helper, a framework's HTTP helpers - is callable as `$this->makeCartWithItems()` inside those tests. Variations: | Call | Effect | |---|---| | `pest()->extend(Base::class)->in('Feature')` | base class for a folder | | `->in('Feature/*Checkout*.php')` | glob patterns select files | | `pest()->extend(Base::class)->use(SomeTrait::class)->in('Feature')` | base class plus traits | | `pest()->extend(Base::class)` in a test file, no `in()` | applies to that file only | `pest()` was introduced as the configuration API in Pest 3. The older `uses(Tests\TestCase::class)->in('Feature')` still works and is common in existing suites. ## `beforeEach()` versus a base-class `setUp()` Both run before every test, and they compose: PHPUnit calls the base class's `setUp()` first, then Pest runs the `beforeEach()` hooks that apply to the test. A framework's base test case typically boots the application in `setUp()`, and your file-level `beforeEach()` then builds the objects this file needs. Put setup that every test in a folder needs into the base class, and setup specific to one file into that file's `beforeEach()`. For clean-up, `afterEach()` runs after every test in its scope, and `->after(fn () => ...)` chained onto a single test adds clean-up for that test only. Both closures are bound to the same instance, so they can reach whatever `beforeEach()` stored on `$this`. ## Mistakes interviewers look for 1. **Static closures.** `static function () { ... }` or `static fn () => ...` cannot be bound to an instance, so Pest cannot give them a `$this` and the test errors instead of running; write plain closures in Pest tests. 2. **Using `$this` in `beforeAll()`.** There is no instance yet. 3. **Expecting state to carry over** from one test to the next through `$this`. Each test gets a fresh object, which is the point. 4. **Putting helpers in `Pest.php` as closures** when they need test-case access; a method on the base test case, wired with `extend()`, keeps them typed and discoverable. 5. **Forgetting `in()`** in `Pest.php`, so the base class is not applied to the intended folder. ## Why interviewers ask this Pest looks like "just functions", and candidates who have only used it through a framework's scaffolding often cannot say what `$this` is. Explaining binding, per-test instances and the base-class wiring shows you understand that Pest is a thin, closure-based layer over PHPUnit's test-case model - which is also what makes mixing `$this->assertSame()` with `expect()` legitimate.

  • In Pest, why is $this unavailable in a test written as static fn () => expect(1)->toBe(1)?
    Pest provides `$this` by binding the test closure to the test case instance, and PHP refuses to bind an instance to a static closure. The binding fails, so the test errors instead of running with a `$this`. Use a plain `function () {}` or a non-static arrow function for Pest tests and hooks.
  • In Pest, when would you still use beforeAll() despite having no $this?
    For preparation that is shared by every test in the file and does not belong to a test case instance, such as creating a temporary directory or warming a read-only fixture file. Anything a test mutates should be built per test in `beforeEach()` so tests stay independent.

saying these in an interview costs you the question

  • Says $this in a Pest test is undefined or a global object
  • Believes $this->cart set in one test is visible in the next
  • Uses $this inside beforeAll()
  • Writes static closures for tests and expects $this to work
  • Thinks pest()->extend() without in() applies to the whole suite