skip to content

In Pest, how do test() and it() differ, and what does describe() add to the tests declared inside it?

level: juniorimportance: should knowfreq 50%

answer

  1. same function underneath
  2. it() prefixes the description
  3. closure is the test body
  4. describe joins names with an arrow
  5. hooks inside describe are scoped

basics

~20 s

test() and it() both register one test from a description and a closure; it() only prefixes the description with 'it'. describe() groups tests under a shared name prefix and scopes the hooks and datasets declared inside it to that group.

solid answer

~50 s

In a Pest file, `test('adds an item', function () { ... })` registers a test whose body is the closure; `it('adds an item', ...)` is the same call with `it ` prepended to the description, so it reads as a sentence: "it adds an item". Pick one style per suite. `describe('Cart', function () { ... })` wraps related tests: their names are prefixed (`Cart → it adds an item`), a `beforeEach()` or `afterEach()` declared inside runs only for tests in that block, and a dataset attached with `->with()` on the `describe()` call feeds every test inside. Blocks nest; hooks run in declaration order, each only for tests in its scope, so a file-level hook at the top runs before a block's own. Pest turns each file into a generated PHPUnit test class, so each `test()`/`it()` becomes one test method and runs through PHPUnit.

code

php · 20 lines
php
<?php
declare(strict_types=1);

use App\Shop\Cart;

describe('Cart', function () {
    beforeEach(function () {
        $this->cart = new Cart();
    });

    it('starts empty', function () {
        expect($this->cart->count())->toBe(0);
    });

    test('adding an item increases the count', function () {
        $this->cart->add('SKU-1', 2);

        expect($this->cart->count())->toBe(2);
    });
});

go deeper

for a junior

Recall that test() and it() register the same closure test, it() adds the word 'it', and describe() groups tests under a shared prefix.

for a middle

Explain how hooks and datasets are scoped to a describe() block and in which order nested hooks run.

for a senior

Show how you structure a converted suite with describe() so failure names carry context without deep nesting or scattered setup.

for a principal

Set naming and grouping conventions for a team so test output reads as a specification and stays consistent across files.

## The two registration functions Pest is a testing layer on top of PHPUnit in which a test is a **closure** rather than a method on a class. You register one with a global function: ```php test('adds an item to the cart', function () { $cart = new Cart(); $cart->add('SKU-1', 2); expect($cart->count())->toBe(2); }); ``` `it()` is the same thing with one difference: it prepends `it ` to the description. Pest's own source defines `it()` as a call to `test()` with `sprintf('it %s', $description)`: ```php it('adds an item to the cart', function () { /* ... */ }); // reported as: it adds an item to the cart ``` So the choice is purely about how names read in the output. Teams usually settle on `it()` for behaviour-style names and `test()` for noun-style names, and keep one style per suite. ## What happens to a test file Pest does not run closures on their own. For each test file it generates a **PHPUnit test class** (a final class extending `PHPUnit\Framework\TestCase` by default, or the class you configure) and turns every `test()`/`it()` call into a method of that class. Inside the closure, `$this` is the test case instance, which is why `$this->assertSame()` still works next to `expect()`. Everything PHPUnit does - `phpunit.xml`, filtering, reporting - applies to Pest tests as well. ## `describe()`: grouping `describe()` takes a description and a closure containing tests: ```php describe('Cart', function () { beforeEach(function () { $this->cart = new Cart(); }); it('starts empty', function () { expect($this->cart->count())->toBe(0); }); describe('when an item is added', function () { it('counts it', function () { $this->cart->add('SKU-1', 1); expect($this->cart->count())->toBe(1); }); }); }); ``` It adds three things: - **A name prefix.** Tests are reported as `Cart → it starts empty` and `Cart → when an item is added → it counts it`, so failures carry their context. - **Scoped hooks.** A `beforeEach()` or `afterEach()` inside the block runs only for the tests in it. Hooks run in the order they are declared, so a file-level hook written at the top of the file runs before the block's own. - **Shared datasets.** `describe(...)->with([...])` passes the dataset to every test inside, and `beforeEach()->with([...])` inside a block does the same. ## `test()` versus `it()` versus `describe()` | Function | Registers | Name in output | |---|---|---| | `test('adds an item', fn)` | one test | `adds an item` | | `it('adds an item', fn)` | one test | `it adds an item` | | `describe('Cart', fn)` | a group, no test itself | prefix `Cart → ...` for tests inside | ## Things candidates get wrong 1. Thinking `it()` and `test()` behave differently at runtime. They do not; only the description changes. 2. Expecting a `beforeEach()` inside one `describe()` to run for tests outside it. Scope follows the block. 3. Declaring variables in the `describe()` body and reading them in tests. The describe closure runs while tests are being collected, not before each test, so per-test state belongs in `beforeEach()` on `$this`. 4. Forgetting that each test file becomes a class. The generated class name is derived from the file path (for `tests/Unit/CartTest.php` it lives under a `P\Tests\Unit` namespace), and that name is what appears when PHPUnit reports or filters tests. ## When grouping pays off In a converted cart suite, `describe('Cart')` with nested blocks such as `describe('discounts')` and `describe('checkout')` replaces what used to be several PHPUnit classes or long method names. Keep nesting shallow - two levels is usually enough - because each level lengthens every failure message and spreads setup across several `beforeEach()` calls that a reader has to assemble in their head. ## Calls you chain onto a test Because `test()` and `it()` return a pending test object, per-test options are chained rather than declared as attributes: - `->with([...])` attaches a dataset; - `->throws(InvalidArgumentException::class)` expects the body to end with an exception; - `->group('cart')` puts the test in a group for `--group` selection; - `->skip('reason')` skips it, `->only()` runs only focused tests while you iterate, and `->todo()` marks it as not yet written; - `->after(fn () => ...)` adds clean-up for this one test. The same chaining style applies to `describe()`, so a whole block can be grouped or given a dataset at once. ## Interview summary `test()` and `it()` register the same kind of closure test; `it()` just reads as a sentence. `describe()` groups tests, prefixes their names, and scopes hooks and datasets to the group.

  • In a Pest file, why does a variable declared directly in a describe() closure not give each test a fresh cart?
    The describe closure runs once while Pest collects the tests, not before each test. A variable created there reaches a test only if captured with `use`, and then every test shares that one object, so state leaks between them. Per-test state goes on `$this` in a `beforeEach()` inside the block.
  • In Pest, in what order do a beforeEach() at the top of the file and a beforeEach() inside a describe() run?
    Hooks run in the order they are declared, each only for tests in its scope. With the file-level hook at the top, it runs first and the block's hook second, for every test inside the block; tests outside get only the file-level hook. The inner hook can therefore build on state the outer one prepared.

saying these in an interview costs you the question

  • Believes it() and test() run tests differently
  • Thinks a beforeEach() inside describe() applies to the whole file
  • Puts per-test setup in the body of the describe() closure
  • Says Pest tests do not run through PHPUnit
  • Nests describe() blocks many levels deep for every small variation