In PHPUnit 13, how does a TestCase subclass declare its tests, and when do setUp and tearDown run?
answer
- extends PHPUnit\Framework\TestCase
- public, test prefix or #[Test]
- new instance per test method
- setUp before each, tearDown after each
- no docblock @test in 12+
basics
~20 sA 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 sA 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
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
Know that tests extend TestCase, that public test-prefixed or #[Test] methods run, and that setUp runs before each test.
Explain the fresh instance per test, the order setUpBeforeClass, setUp, test, tearDown, tearDownAfterClass, and why the constructor is off limits.
Keep fixtures minimal and per-test, reserve setUpBeforeClass for expensive read-only resources, and migrate annotation-based suites to attributes.
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.