skip to content

With PHPUnit 13, how does #[Depends] pass a value between test methods, and what happens when the producer test fails?

level: middleimportance: nice to knowfreq 22%

answer

  1. producer's return value becomes an argument
  2. appended after data-provider arguments
  3. failed producer means skipped consumer
  4. objects shared by handle unless cloned
  5. DependsUsingDeepClone, DependsUsingShallowClone

basics

~20 s

#[Depends('testProducer')] makes a test receive the producer's return value as an argument and run only if the producer passed in the same run. A failed, skipped or unrun producer makes PHPUnit skip the dependent test.

solid answer

~40 s

`#[Depends('testLoadsPolicy')]` declares that a test needs another test to have passed; the producer's return value is passed to the consumer as an argument, after any data-provider values. If the producer failed, was skipped, or did not run at all in this run - for example because `--filter` selected only the consumer - PHPUnit skips the consumer with "This test depends on ... to pass". A dependency on a method that does not exist is an error. Returned objects are passed by handle, so several consumers share one object unless you use `#[DependsUsingShallowClone]` or `#[DependsUsingDeepClone]`. `#[DependsExternal]` and `#[DependsOnClass]` reach another class. Use it sparingly: chains make tests order-dependent.

code

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

namespace App\Tests\Security;

use App\Security\PasswordPolicy;
use App\Security\PasswordStrength;
use PHPUnit\Framework\Attributes\DependsUsingDeepClone;
use PHPUnit\Framework\TestCase;

final class PasswordPolicyTest extends TestCase
{
    public function testLoadsPolicyFromConfig(): PasswordPolicy
    {
        $policy = PasswordPolicy::fromArray(['minLength' => 12, 'requireDigit' => true]);
        $this->assertSame(12, $policy->minLength);

        return $policy;
    }

    #[DependsUsingDeepClone('testLoadsPolicyFromConfig')]
    public function testRejectsShortPassword(PasswordPolicy $policy): void
    {
        $this->assertFalse((new PasswordStrength($policy))->isStrong('Ab1!'));
    }
}

go deeper

for a junior

Recall that #[Depends] passes the producer's return value to the consumer and skips the consumer when the producer did not pass.

for a middle

Explain argument order with a data provider, object sharing by handle, the clone variants, and why a filtered run skips the consumer.

for a senior

Judge when a dependency chain is worth its coupling and when setUp() or a builder keeps tests independent and parallel-safe.

for a principal

Set a suite-wide convention: dependencies only for genuine precondition chains, so reordering and parallel execution stay safe as the suite grows.

## What #[Depends] declares `#[Depends]` from `PHPUnit\Framework\Attributes` records a **dependency between test methods**: the consumer test needs the producer test to have passed. It is repeatable and targets methods. The argument is the producer's method name in the same class: ```php public function testLoadsPolicyFromConfig(): PasswordPolicy { $policy = PasswordPolicy::fromArray(['minLength' => 12, 'requireDigit' => true]); $this->assertSame(12, $policy->minLength); return $policy; } #[Depends('testLoadsPolicyFromConfig')] public function testRejectsShortPassword(PasswordPolicy $policy): void { $this->assertFalse((new PasswordStrength($policy))->isStrong('Ab1!')); } ``` Two things happen: the producer's **return value** is handed to the consumer as an argument, and the consumer's execution is **conditional** on the producer's result. ## What happens on failure PHPUnit keeps a record of tests that passed in the current run. Before running a consumer it checks that record: - **Producer passed**: the consumer runs with the return value. - **Producer failed, errored or was skipped**: the consumer is **skipped** with the message `This test depends on "…::testLoadsPolicyFromConfig" to pass`. - **Producer never ran** in this invocation: also skipped, for the same reason. Running a single consumer with `--filter` is the classic way to hit this. - **Producer does not exist** (typo, renamed method): the consumer is reported as an **error** saying it depends on something "which does not exist". The skip is the point: one broken precondition shows as one failure plus a few skips, instead of a cascade of confusing failures. ## Argument order with data providers When a test has both a data provider and `#[Depends]`, PHPUnit builds the argument list as **data-set values first, then the return values of the producers** in declaration order. The consumer's signature must follow that order: `testX(string $password, bool $expected, PasswordPolicy $policy)`. ## Sharing, cloning and the other variants | Attribute | Passes | |---|---| | `#[Depends('m')]` | the return value itself; an object is the same handle | | `#[DependsUsingShallowClone('m')]` | `clone` of a returned object | | `#[DependsUsingDeepClone('m')]` | a deep copy of the returned value | | `#[DependsExternal(Other::class, 'm')]` | a producer method in another test class | | `#[DependsOnClass(Other::class)]` | requires every test in `Other` to have passed | PHP passes objects by **handle**, so with plain `#[Depends]` two consumers of the same producer receive the very same object. If the first mutates it, the second sees the mutation, and the result depends on run order. The clone variants give each consumer its own copy; a shallow clone still shares nested objects, a deep copy does not. `DependsExternal` and `DependsOnClass` also have shallow and deep clone variants. ## Ordering By default PHPUnit resolves dependencies when it orders tests, moving producers ahead of their consumers. The ordering options themselves belong to the runner configuration; what matters here is that `#[Depends]` is a real ordering constraint, not just documentation. ## Should you use it? Interviewers ask this as a judgement question. `#[Depends]` is useful for a genuine **precondition chain** - a parser test that must pass before tests that consume its output - because it turns a cascade into skips. It is costly as a way to share fixtures: 1. Tests stop being runnable alone; a filtered run skips the consumer. 2. Shared objects couple tests through mutation unless cloned. 3. Parallel runners and reordering tools must respect the chain. For shared setup, a `setUp()` method or a small builder helper keeps each test independent. Reserve `#[Depends]` for the case where "if this failed, the rest are meaningless" is literally true. In the password-strength example, a reasonable reading is: if the policy cannot even be loaded from configuration, every strength check built on it is noise, so skipping them is more informative than failing them. If, on the other hand, the only reason for the chain is to avoid writing `PasswordPolicy::fromArray()` twice, a private helper method in the test class does the same job without making the checks depend on run order or on a filter that happens to include the producer. ## Migration note The old `@depends` docblock tag, including its `clone` and `shallowClone` forms, is not read by PHPUnit 13. A leftover tag means no dependency at all: the consumer runs in whatever order, and if it declares a parameter for the producer's value, PHP throws `ArgumentCountError` because nothing is passed.

  • In PHPUnit 13, you run vendor/bin/phpunit --filter testRejectsShortPassword and the test is skipped. Why?
    The filter selected only the consumer, so its `#[Depends]` producer never ran in this invocation. PHPUnit records only tests that passed in the current run, finds no pass for the producer, and skips the consumer with 'This test depends on ... to pass'. Widen the filter to include the producer.
  • With PHPUnit 13, when would you choose #[DependsUsingDeepClone] over plain #[Depends]?
    When the producer returns a mutable object and more than one consumer uses it. Plain `#[Depends]` hands every consumer the same object handle, so a mutation in one leaks into the next. A deep copy isolates each consumer; a shallow clone isolates only the top-level object and still shares nested ones.

saying these in an interview costs you the question

  • Thinks #[Depends] only orders tests and passes nothing between them
  • Expects the consumer to run with null when the producer fails
  • Believes each consumer automatically gets a fresh copy of a returned object
  • Uses #[Depends] chains as the default way to share fixtures
  • Assumes a filtered run will pull the producer in automatically