How to make Upgrade Safe with Bridge Testing

Upgrading can go smooth without no bugs and just work. We can make our customer happy, even though we don't have any tests.

The older I am, the more I care about safety. Not just for now, but for tomorrow and for my safety of my colleagues. Also for the developers, who will work on the project even though I'm long gone.

That's why before I start upgrading one approach to another, I want to prepare a safe environment. No razors, no matches, and a couple of tests. The bridge testing technique is one of the safety nets I use while refactoring to new technology.

The idea is simple - in total, we should have 3 tests:


It will be more clear from an example. Let's say we use @AttentionPrice annotation to express the amount of attention we should spend while reading the code:

/**
 * @Annotation()
 */
class AttentionPrice
{
    private int $amount;

    public function __construct(array $values)
    {
        $this->amount = $values['amount'];
    }

    public function getAmount(): int
    {
        return $this->amount;
    }
}

In our code, we'll use annotation to express required attention for specific elements:

final class DesignPattern
{
    /**
     * @AttentionPrice(1000)
     */
    public $publicProperty;

    /**
     * @AttentionPrice(100)
     */
    private $privateProperty;
}

Here we can see that public property takes much more attention than private one. Public properties can be used in the class, outside the class, and changed anytime. On the other hand, private property can be used exclusively in this class.

Bridge Test

Let's add a bridge test so we are sure the refactoring works for both annotations and attributes. How would the bridge test look like for public property?

use PHPUnit\Framework\TestCase;

final class BridgeTest extends TestCase
{
    private Reader $reader;

    protected function setUp(): void
    {
        // create Reader instance or get it from container
        $this->reader = // ...;
    }

    public function testCurrent(): void
    {
        $resolvedValue = $this->reader->resolveAnnotationValue(
            'DesignPattern', 'publicProperty'
        );
        $this->assertSame($resolvedValue, 1000);
    }
}

Let's run the test:

vendor/bin/phpunit tests/BridgeTest.php

It should pass - it only confirms already existing behavior.


Let's add a new method, resolveAttributeValue() that will be able to read PHP 8 attribute values too:

    // ...

    public function testNew(): void
    {
        $resolvedValue = $this->reader->resolveAttributeValue(
            'DesignPattern', 'publicProperty'
        );
        $this->assertSame($resolvedValue, 1000);
    }

    // ...

Now the test should fail because we have yet to implement the resolveAttributeValue() method:

vendor/bin/phpunit tests/BridgeTest.php

Last but not least, the bridge test compares the results of both methods:


    // ...

    public function testBridge(): void
    {
        $resolvedAnnotationValue = $this->reader->resolveAnnotationValue(
            'DesignPattern', 'publicProperty'
        );
        $resolvedAttributeValue = $this->reader->resolveAttributeValue(
            'DesignPattern', 'publicProperty'
        );

        $this->assertSame($resolvedAnnotationValue, $resolvedAttributeValue);
    }

    // ...

Why Not Just The Bridge Test?

Looking at the test, it seems the testBridge() already includes the test of their former two methods. Why not delete those two? ... Wait, wouldn't that be like cutting our safety ropes?


At first, the test passes, and both methods return 1000 and assertSame() works correctly.

$resolvedAnnotationValue = ...; // 1000
$resolvedAttributeValue = ...; // 1000
$this->assertSame($resolvedAnnotationValue, $resolvedAttributeValue);

Later that day, we do a bit of refactoring. Let's run tests now:

vendor/bin/phpunit tests/BridgeTest.php

It still passes, yay!


A few weeks later, we find out the production annotations and attributes were ignored, your admin was publicly available, and the test passes with these values:

$this->assertSame(null, null);

That's why it's essential to have two previous methods and compare exact values. Now that we have safety rules defined let's start the dirty work!


Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!