
Control Your Legacy Refactor With PEST Architecture Testing
PEST started in the imaginations of Nuno Maduro and Luke Dowling in 2021, with the mission to seemingly bring the JEST-like style of testing from the JavaScript world into the PHP world through Laravel. Since then, there have been two major releases of the project and plenty of new features to raise eyebrows.
For me, it was the introduction of architecturetesting that I’d not seen before. Sure, you should write tests for absolutely everything you do (and you should!), but what use would be to write tests about the actual structure of the application itself? Well, from starting a greenfield project: probably not a whole lot. But here’s where the beauty of it came in: what about introducing it into a big old legacy plate of spaghetti that needs managed refactoring? Now we’re talking! In this article, we’ll look at the easiest starting points to kick off a legacy refactor with PEST Architecture Testing.
One for the Engineering Managers
What kind of stuff can it do? Well, using the arch() access function, we can do several things with the PEST Expectation API:
Define where classes can be extended and when they cannot
Define where your interfaces sit
Define what classes should and should not use traits
Refactor out accidental die() or dump() debugging calls before something like PHPStan/Psalm is fired
Make your application adhere to Laravel’s naming conventions
That’s a lot of things out of the box. So, let’s say you inherit a new project, and in one hour, you can start the refactor with high-level testing. The ideal scenario you’d want when coming into a legacy codebase would, therefore, be:
Write up the style guide of how the new application’s logic should look and work
Start writing failing PEST tests to implement this.
In a way, what I feel is being proposed is a reimplementation of Behaviour Driven Development. Think of the style guide as your Gherkin-like tests, which you then translate into unit and integration tests in PEST.
Lead by Example
All this is academic without examples, so let’s take the theory that you have a legacy Laravel codebase. Before the tests come into play, you’re going to want to either use an auto-refactoring tool like RectorPHP to rule PHP rulesets that will bring the code back up to modern conventions, or you can use Laravel Shift. From here, we can introduce PEST using composer:
composer require pestphp/pest --dev --with-all-dependencies
And you can then initialise a new PEST configuration:
./vendor/bin/pest --init
This will create your Pest.php configuration file, which, if you are familiar with using PHPUnit, is the equivalent of the phpunit.xml config file. It’s time to get to work.
Uh, What Next?
Does Your Code Look Like This?Here is it. The reason you have a career in tech. You live for inheriting a 15-year-old Laravel application that has debugging routes completely exposing core CLI commands, you have circular dependencies, and you’ve probably got DB::raw(‘SELECT * FROM nightmare WHERE refactoring = 1’); all over the place.
Let’s create a rule that any debugging commands accidentally committed into the codebase must go before any progress can be made. That’s a bare minimum, right? This is built into PEST, so first create a test in the command line using the console:
php artisan pest:test DebbugingRulesetTest
Run the test runner:
php artisan test
You’ll notice that it will fail, as PEST has attempted to boilerplate the code. Navigate to the test file DebuggingRulesetTest.php and replace the code like so:
<?php
arch()->preset()->php();
Run the tests, and given that you’ve inherited a theoretical mess: I expect PEST will start moaning at you.
Whoops, No Debugging Code Here, Please!
Alt:
We’ve secured the first refactor rule, now we need to enforce it. There are several ways of doing this; in production environments, it’s common for a Continuous Integrations runner such as Atlassian’s Bitbucket Pipelines, Gitlab’s CI/CD, or GitHub’s Actions to fire rules to be able to merge Pull Requests. We’re, however, going to implement the strictest rules, where you cannot even commit to the code without PEST being satisfied.
This is done through git hooks. Create a new pre-commit hook with this command:
cd my-code/.git/hooks
// Windows Users Only
echo. > pre-commit
// Unix-like Users Only
touch pre-commit
chmod +x pre-commit
Open the pre-commit file created in the previous step in a text editor or IDE, and add the code to run PEST:
#!/bin/sh
echo "Pre Commit Test Runner Fired"
# Detect OS
if [ "$(uname 2>/dev/null)" = "Linux" ] || [ "$(uname 2>/dev/null)" = "Darwin" ]; then u
echo "Running Unix-like"
CMD="./vendor/bin/pest"
else
echo "Running Windows"
CMD="vendor\\bin\\pest.bat"
fi
$CMD
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "Cannot Commit, Tests Failed"
exit 1
fi
echo "Commit Success"
exit 0
The pre-commit file is a reserved file name that git looks at for various actions - in this case, whatever the contents are inside that file will run every time a user attempts to commit to the codebase.
Go ahead, try to commit a change to the code, and watch what happens!
This sets us up nicely to kick off our refactoring as a starting point. We’ve covered the setup, so now it’s really just a matter of utilising PEST Architecture Testing API methods. In our test, we can add a couple more high-level rules:
<?php
arch()->preset()->php(); // our previous rules
arch()->preset()->security();
arch()->preset()->strict();
I’ve added a couple of rules in here, which you can see in detail in the PEST source code. The first one, security(), will aid you heavily in your initial refactor: it takes care of failing your code and identifying where there are serious security holes. These include using the PHP Standard Library methods that have been superseded for security reasons, such as
md5(): MD5-like uses really need to be replaced with SHA256, so if you have data such as session IDs or database unique identifiers, these need to have an ETL-like refactor
rand(): PHP’s original random function isn’t actually random
shell_exec(): Calling the host machine’s shell is, quite possibly the worst thing you can do. You will need to do this, of course, at some point, but the idea here is that you use Laravel’s console and wrapped methods for security rather than letting legacy code rampantly rip your server to pieces
Given the number of rules that are contained within these starting points alone, that’s a couple of sprints worth of refactoring I think! There are all sorts of things I think you could do with this, given that PEST allows for a whole plethora of tests in the Expectation API.
Conclusion
Along with Architecture testing, PEST 3 also saw the introduction of mutation testing which is for another time. The amount of features PEST ships with, though really is a complete arsenal of tooling for those taking on legacy projects. We’re passionate about testing at Vonage, so fancy chatting to us about PHP? Join our thriving Developer Community on Slack, follow us on X (formerly Twitter), or subscribe to our Developer Newsletter. Stay connected, share your progress, and keep up with the latest developer news, tips, and events!