A PHPUnit test passes when run alone but fails when its whole class runs; how can setUpBeforeClass or static properties cause that?
answer
- instance state is per test
- static state lives for the class
- setUpBeforeClass runs once
- mutation by an earlier test
- rebuild in setUp instead
basics
~20 sInstance properties are rebuilt for every test, but static properties and fixtures created in setUpBeforeClass are shared by all tests in the class. An earlier test that mutates them changes what a later test sees.
solid answer
~40 sPHPUnit creates a new test-class instance for every test method, so anything assigned to `$this` in `setUp()` starts fresh. **Static** state does not: a `private static` property, a fixture built in `setUpBeforeClass()`, a static cache inside the class under test, or a registry singleton lives for the whole run of the class, or longer. If `testApplyStackedCodes` adds a code to a shared `DiscountRules` object and `testNoCodesMeansNoDiscount` runs later, the second test sees the leftover code and fails, although it passes when run alone. Fix it by building mutable fixtures in `setUp()`, keeping `setUpBeforeClass()` for expensive **read-only** resources, resetting static caches in `tearDown()`, and designing production code so that global state can be reset or injected.
code
php · 26 lines<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DiscountCalculatorTest extends TestCase
{
private DiscountRules $rules;
protected function setUp(): void
{
// fresh, mutable fixture for every test
$this->rules = new DiscountRules();
}
public function testApplyStackedCodes(): void
{
$this->rules->add('SPRING10');
$this->assertSame(90, (new DiscountCalculator($this->rules))->total(100));
}
public function testNoCodesMeansNoDiscount(): void
{
$this->assertSame(100, (new DiscountCalculator($this->rules))->total(100));
}
}go deeper
Know that setUp gives each test fresh instance properties, while setUpBeforeClass and static properties are shared.
Explain why a test passes alone but fails in the class: a static fixture or cache mutated by an earlier test.
Diagnose order dependence systematically, move mutable fixtures into setUp, confine setUpBeforeClass to read-only resources, and remove static state from production code.
Treat order-dependent tests as defects across the codebase, and weigh global state in production design against the test isolation it breaks.
## The symptom A test passes with `--filter testNoCodesMeansNoDiscount` and fails when the whole `DiscountCalculatorTest` class runs. Nothing in the test changed. What changed is **what ran before it**. The test depends on state that another test left behind. ## Where shared state hides PHPUnit isolates **instance** state by construction: every test method gets a **new object** of the test class, and `setUp()` runs on that object. The following survive from one test to the next: | Source | Lifetime | |---|---| | properties assigned in `setUp()` | one test (fresh each time) | | a fixture created in `setUpBeforeClass()` and kept in a `static` property | every test in the class | | a `static` property of the test class | every test in the class, and any test that reads it later | | a static cache, registry or singleton inside the **code under test** | the whole PHP process, across classes | | `$GLOBALS`, environment variables, files on disk, rows in a database | until something resets them | `setUpBeforeClass()` is **static** and runs **once**, before the first test of the class. It is meant for expensive resources that tests only read. As soon as one test mutates such a fixture (adds a discount rule, changes a configuration value, pushes onto a list), every later test in the class inherits the change. ## A worked example ```php final class DiscountCalculatorTest extends TestCase { private static DiscountRules $rules; public static function setUpBeforeClass(): void { self::$rules = new DiscountRules(); } public function testApplyStackedCodes(): void { self::$rules->add('SPRING10'); // mutates the shared fixture // ... } public function testNoCodesMeansNoDiscount(): void { $this->assertSame(100, (new DiscountCalculator(self::$rules))->total(100)); } } ``` Run alone, the second test sees an empty rule set and passes. Run after the first, it sees `SPRING10` and gets `90`. ## Diagnosing it 1. **Reproduce with the smallest pair.** Run the failing test together with each earlier test of the class until the pair that fails is found. 2. **Look for `static`** in the test class and in the classes it touches, and for anything created in `setUpBeforeClass()`. 3. **Check the code under test** for static caches and singletons: a `static array $cache` inside `DiscountRules::load()` leaks across test classes too. 4. **Run the suite in a different order.** Order-dependent failures change when the order does, which confirms the diagnosis. Configuring random execution order belongs to PHPUnit's XML configuration, a separate topic. ## Fixing it - **Move mutable fixtures to `setUp()`.** Building a `DiscountRules` object per test costs microseconds; debugging order-dependent failures costs hours. - **Keep `setUpBeforeClass()` for read-only, expensive resources,** such as parsing a large fixture file once, and treat the result as immutable. `readonly` properties or immutable value objects make accidental mutation impossible. - **Reset what you cannot avoid** in `tearDown()` or `tearDownAfterClass()`: clear the static cache, delete temporary files, restore environment variables. - **Change the design** where a static singleton makes tests fight: inject the dependency so each test passes its own instance. - PHPUnit can back up and restore static properties and globals between tests through attributes and configuration, but that treats the symptom and costs time. Removing the shared state is the better fix. ## A checklist before adding a class-level fixture 1. Is it genuinely expensive, meaning hundreds of milliseconds rather than a constructor call? 2. Does every test in the class **only read** it? If one test writes, the fixture belongs in `setUp()`. 3. Can it be made immutable, with `readonly` properties or a value object, so that a later edit cannot mutate it by accident? 4. Is it released in `tearDownAfterClass()` (connections closed, temporary directories removed)? 5. Would a single test still pass when run on its own, first, last or in reverse order? If any answer is no, build the fixture per test. The time saved by sharing a cheap object is never worth an order-dependent suite. ## Why this matters beyond one class Static state in production code leaks across test **classes** as well as tests, because the whole suite usually runs in one PHP process. A suite that is green in one order and red in another is not trustworthy, which is why teams treat order dependence as a bug even when the current order happens to pass.
- When is setUpBeforeClass the right tool?When a resource is expensive to create and every test only reads it, such as a large fixture file parsed once or an immutable lookup table. Store it in a static property, never mutate it, and release it in `tearDownAfterClass()`. If any test needs to change it, build that part per test in `setUp()` instead.
- The leak comes from a static cache inside the production class. What are the options?The best option is a design change: inject the cache or the rules object, so each test passes its own. If that is not possible yet, expose a reset method used only by tests and call it in `tearDown()`, accepting it as debt. Backing up static properties between tests also works but slows the suite and hides the design problem.
saying these in an interview costs you the question
- Properties set in setUp are shared by all tests in the class.
- setUpBeforeClass runs before every test method.
- PHPUnit runs each test in a separate PHP process by default.
- Static caches reset automatically between test classes.
- If tests pass in the current order, order dependence is harmless.