Introducing the TestTag attribute

November 28th 2025


Code showing the TestTag attribute usage

Years ago I studied electronic engineering at the University of Bristol. Most electronic systems are composed of one or more chips (also known as Integrated Circuits - ICs). The ICs had connectors (pins or tags) to take inputs and give outputs. Some of these ICs were very complex and testing them was difficult. They were both metaphorically and literally black boxes. To aid testing some ICs would have pins or tags that were only used for testing. These could act either as inputs; to put the chip into a certain state. Or outputs, to get an, otherwise, internal value.

It was these test pins or tags that were the inspiration for the #[TestTag] attribute.

The #[TestTag] is added to methods to signify these methods should only be used for testing purposes.

In one legacy project I work on our models use IDs that are set by the database. We have unit tests for some of these models. The unit tests require the ID field to be set. No real database is available for the unit tests. To get round this problem we added a setId method to the models. These methods should only ever be called by test code and never application code. The TestTag enforces this requirement.

E.g. the Person::setId method can only be called from test code.

class Person {

    // Set by database when creating the object in the database
    private int $id;

    #[TestTag]
    public function setId(int $id): void {
        $this->id = $id;
    }
}

$person = new Person();

// ❌ Calling methods with TestTag attribute is not allowed outside of tests.
$person->setId(10); 

This would be allowed:

final class PersonTest extends TestCase {

    public function testHappyPath(): void
    {
        $person = new Person():

        // ✅ Calling from a test case, this is allowed. 
        $person->setId(10); 

        // ... remaining test ...
    }
}

Determining which code is test code

If you are using the PHPStan PHP Language Extension plugin you can define what is test code in the following ways:

Assume all classes ending with Test are test code

Adding the following to your phpstan.neon file will mean that all classes that end with Test will be deemed test code.

parameters:
  phpLanguageExtensions:
    mode: className

Assume all code in a certain namespace is test code

Adding the following to your phpstan.neon file allows you to specify the base namespace that all test code lives in. In the example below any code in a namespace starting App\Test is deemed test code.

parameters:
  phpLanguageExtensions:
    mode: namespace
    testNamespace: 'App\Test'

See the PHPStan extension's README for full details.

TestTag vs other approaches

It is possible in PHP to access private member data, and there are libraries that support this.
In the example above we could not provide the setId. Instead, we'd use a library, or write PHP code, to bypass PHP's visibility checks and set Person::id directly.

Rather than clever hacks that access private member data, I'd rather access is done via methods with the TestTag attribute for the following reasons:

  • It makes everything explicit. Developers can see that, in this example, the setId method is used for testing only.
  • If at some stage in the future no tests require the setId method, our IDE, or static analysis tools, will show that the method is no longer used and safe to delete.
  • It offers encapsulation, if the property name is updated, none of the tests need change, as they'll still access the property via the setId method.

Summary

The TestTag can simplify or enable testing by exposing internal state within an object.

The use of TestTag probably indicates a code smell. Every time you use it you should ask yourself, is there a better way of doing this. That said, for gradually improving legacy code, TestTag could prove very useful.

NOTE: In this setId example, I'd argue a better solution is to use a database friendly UUID for the model's ID, with the application and not the database responsible for generation the ID.

Related articles