skip to content

In PHPUnit 13, how does a TestCase subclass declare its tests, and when do setUp and tearDown run?

level: juniorimportance: should knowfreq 58%

answer

  1. extends PHPUnit\Framework\TestCase
  2. public, test prefix or #[Test]
  3. new instance per test method
  4. setUp before each, tearDown after each
  5. no docblock @test in 12+

basics

~20 s

A test class extends PHPUnit\Framework\TestCase; each public method whose name starts with test, or that carries #[Test], is a test. PHPUnit creates a fresh instance per test, calling setUp before and tearDown after each test.

solid answer

~40 s

A test class extends `PHPUnit\Framework\TestCase`. A method is a test when it is **public** and its name starts with lowercase `test`, or when it carries the `#[Test]` attribute (`PHPUnit\Framework\Attributes\Test`). PHPUnit 12 and 13 read attributes only; the old `@test` docblock annotation is ignored. For every test method PHPUnit builds a **new instance** of the class, then runs `protected function setUp(): void`, the test, and `tearDown(): void`. tearDown runs whether the test passed, failed or errored. So a property assigned in `setUp()`, such as `$this->calculator = new DiscountCalculator()`, is fresh for every test. `TestCase`'s constructor is `final`, so setup belongs in `setUp()`, not in a constructor. The static `setUpBeforeClass()` and `tearDownAfterClass()` run once per class.

code

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

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class DiscountCalculatorTest extends TestCase
{
    private DiscountCalculator $calculator;

    protected function setUp(): void
    {
        $this->calculator = new DiscountCalculator();
    }

    public function testTenPercentOffHundredIsNinety(): void
    {
        $this->assertSame(90, $this->calculator->apply(100, 10));
    }

    #[Test]
    public function zeroPercentLeavesPriceUnchanged(): void
    {
        $this->assertSame(100, $this->calculator->apply(100, 0));
    }
}

go deeper

for a junior

Know that tests extend TestCase, that public test-prefixed or #[Test] methods run, and that setUp runs before each test.

for a middle

Explain the fresh instance per test, the order setUpBeforeClass, setUp, test, tearDown, tearDownAfterClass, and why the constructor is off limits.

for a senior

Keep fixtures minimal and per-test, reserve setUpBeforeClass for expensive read-only resources, and migrate annotation-based suites to attributes.

for a principal

Set conventions for test structure across a codebase: naming, attribute use, shared base classes and how much setup is allowed.

## Anatomy of a test class A PHPUnit test is a method on a class that extends **`PHPUnit\Framework\TestCase`**. `TestCase` extends `PHPUnit\Framework\Assert`, which is where `assertSame`, `assertCount` and the other assertions come from. By convention the class is named after the class under test with a `Test` suffix (`DiscountCalculatorTest`) and declared `final`. ## Which methods are tests PHPUnit looks at the **public** methods declared in the test class. A method is a test when either: 1. its name **starts with `test`**, case-sensitively (`testAppliesTenPercent`), or 2. it has the **`#[Test]`** attribute from `PHPUnit\Framework\Attributes\Test`, which frees the name (`appliesTenPercent`). Consequences worth knowing: - A `private` or `protected` method is never a test, even with `#[Test]`. - A public helper that happens to be named `testData()` **is** a test, because it starts with `test`. - `TestSomething` with a capital T is not matched by the prefix rule. - The docblock annotation `/** @test */` is ignored: PHPUnit 12 removed annotation support and PHPUnit 13 reads metadata from attributes only. - A class with no tests produces the warning `No tests found in class "..."`, and a method that is both a hook and a test is rejected with a warning. ## The per-test lifecycle For **each** test method, PHPUnit: 1. creates a **new instance** of the test class (`TestCase`'s constructor is `final`, so you cannot hook in there); 2. calls **`setUp()`** to build the fixture; 3. calls `assertPreConditions()`, then the test method, then `assertPostConditions()` (rarely overridden); 4. calls **`tearDown()`**, which runs even when the test failed or errored. Around the whole class, the **static** methods `setUpBeforeClass()` and `tearDownAfterClass()` run once: before the first test and after the last one. | Method | Signature | Runs | |---|---|---| | `setUpBeforeClass` | `public static function setUpBeforeClass(): void` | once, before the first test of the class | | `setUp` | `protected function setUp(): void` | before every test | | `tearDown` | `protected function tearDown(): void` | after every test, pass or fail | | `tearDownAfterClass` | `public static function tearDownAfterClass(): void` | once, after the last test of the class | The `#[Before]`, `#[After]`, `#[BeforeClass]` and `#[AfterClass]` attributes mark additional hook methods with any name, for fixtures shared through traits. ## What that means in practice Because every test gets its own object, **instance properties are isolated**. A `DiscountCalculator` created in `setUp()` and stored in `$this->calculator` cannot carry state from one test to the next. Declaring the property with a type and assigning it in `setUp()` is the standard pattern: - keep `setUp()` to what **every** test in the class needs; a fixture only one test uses belongs in that test; - call `parent::setUp()` when extending a base test class that has its own setup; - use `tearDown()` for resources the garbage collector will not handle, such as temporary files, and not to `unset()` properties that die with the instance anyway. **Static** properties and anything set up in `setUpBeforeClass()` are the exception: they live as long as the class, so every test shares them. That is the usual source of tests that pass alone and fail together. ## Mistakes that show up in review - **Changing the hook signature.** `setUp()` must stay compatible with `TestCase::setUp(): void`. Dropping the `: void` return type is a fatal *declaration must be compatible* error before any test runs. - **Forgetting `parent::setUp()`** in a class that extends a shared base test case, so the base fixture is never built. - **Public helpers named `test...`.** A `testData()` builder becomes a test of its own, usually a failing or risky one. Name helpers `createDiscount()`, `givenRules()` and so on. - **Setup that only some tests need.** Every test pays for it, and a reader cannot tell which parts matter to which test. - **Assertions inside `setUp()`.** They run before every test and turn a fixture problem into a failure blamed on whichever test happened to run. ## Running it `vendor/bin/phpunit tests/DiscountCalculatorTest.php` runs one file. How suites, bootstrap and filters are configured belongs to `phpunit.xml`, a separate topic.

  • Why can't you set up the fixture in the test class's constructor?
    `TestCase::__construct()` is declared `final`, so it cannot be overridden. PHPUnit also creates one instance per test method, so `setUp()` runs at the right moment for each test and keeps fixture code in one documented hook.
  • A method annotated with /** @test */ no longer runs after upgrading to PHPUnit 13. Why?
    PHPUnit 12 removed support for docblock annotations, and PHPUnit 13 reads test metadata from attributes only. Rename the method with a `test` prefix or add `#[Test]` from `PHPUnit\Framework\Attributes\Test`, and convert the other annotations to their attribute equivalents.
  • Does tearDown run when the test fails?
    Yes. PHPUnit invokes `tearDown()` after the test body whether it passed, failed an assertion or threw an error, so cleanup of files or connections in `tearDown()` is reliable. An exception thrown inside `tearDown()` itself is reported only if the test had not already failed.

saying these in an interview costs you the question

  • All test methods in a class share one instance.
  • A protected method with #[Test] runs as a test.
  • The /** @test */ docblock still marks tests in PHPUnit 13.
  • tearDown is skipped when an assertion fails.
  • Override the constructor to build the fixture once.