JesusValera

Why you should not use Reflection when testing

with some code examples

 Found a typo? Edit me

the-art-of-programming-meme

It causes bugs when refactoring due to the high coupling

When we use reflection, our tests get too fragile, we are allowing our tests to know so much information about the real implementation. We need to hardcode the method name, and we are coupling our test method to the production code. Furthermore, we need to write a lot about boilerplate to test a simple method.

So, we do not need to test the private methods per se; they are called indirectly from our public functions.

Comparison between standard and Reflection tests

final class Addition
{
    public function simpleOperation(int $number1, int $number2): int
    {
        return $number1 + $number2;
    }
}

final class Operation
{
    public function addition(int $number1, int $number2): int
    {
        return (new Addition())->simpleOperation($number1, $number2);
    }
}

final class OperationTest
{
    // Without Reflection.
    public function testSimpleOperationWithoutReflection(): void
    {
        $operation = new Operation();
        $this->assertSame(4, $operation->addition(2, 2));
    }

    // With Reflection.
    public function testSimpleOperationWithReflection(): void
    {
        $addition = $this->createPartialMock(Addition::class, [
            'simpleOperation',
        ]);

        $result = 2 + 2;
        $addition
            ->expects($this->once())
            ->method('simpleOperation')
            ->willReturn($result);

        $operation = new \ReflectionClass(Operation::class);
        $reflection = $operation->invoke($addition);

        $this->assertSame($result, $reflection->addition());
    }
}

What would happen if we modified the simpleOperation() method from the Addition class to the following, and we run the tests?

final class Addition
{
   public function simpleOperation(int $number1, int $number2): int
   {
       return $number1 - $number2;
   }
}

Dealing with some problems when not using Reflection

Let me show you a more realistic example (idea from PHPTheRightWay):

final readonly class Vehicle
{
    public string $model;
    public int $price;

    public function __construct(string $model)
    {
        $this->model = $model;
        $this->price = random_int(1000, 3000);
    }
}

To test the vehicle’s model is easy, but what about the price?

public function testModel(): void
{
    $model = 'Seat';
    $vehicle = new Vehicle($model);

    $this->assertSame($model, $vehicle->model());
}

public function testPrice(): void
{
    $model = 'Seat';
    $vehicle = new Vehicle($model);

    $this->assertSame( ¿¿?? , $vehicle->price());
}

One possible solution could be using the Reflection class to be able to set explicitly the price like:

public function testGetPrice(): void
{
     /** @var Vehicle|MockObject $vehicle */
     $vehicle = $this->createPartialMock(Vehicle::class, []);

     $reflection = new \ReflectionProperty(Vehicle::class, 'price');
     $reflection->setAccessible(true);
     $price = 200;
     $reflection->setValue($vehicle, $price);

     $this->assertSame($price, $vehicle->price());
}

But our Vehicle class is final, so we cannot perform this test, also, we said we shouldn’t use the Reflection class, so, probably we are doing something wrong in this class (TIP: you should make final your classes by default 😉).

One solution could be to inject the value in the constructor like:

final readonly class Vehicle
{
    public function __construct(
        public string $model,
        public int $price,
    ) {
    }
}

And when we want to create this class, we could pass the final price as:

$vehiclePrice = random_int(1000, 3000);
$vehicle = new Vehicle(Seat’, $vehiclePrice);

So, our price test could be like:

public function testPrice(): void
{
    $price = 200;
    $vehicle = new Vehicle('foo', $price);

    $this->assertSame($price, $vehicle->price());
}

To sum up, I don’t recommend using the Reflection class anywhere in your code unless you are very aware of what you are doing, usually, there are alternative implementations to what you want to achieve without using it.

stone-figures

Additionally, defining our classes as final helps us to have a better design, not only because it forbid us the use of Reflection, but also it prevents us from mocking our business logic, which is good.