Defragment your PHPUnit tests

January 24, 2024

Let's mash up two concepts: automated tests and disk defragmentation.

MS-DOS 6.x included a defrag utility that honestly was just so satisfying to watch. The 90s were a different time, folks. Disk defragmentation took about an hour and physically rearranged the data on your hard drive so it was more efficient to read off the disk.

When coding, we spend a lot of time looking at test output, and it occurred to me that someone should figure out how to make our test output look more like a defragmenter to keep things interesting. I didn't get any takers on Twitter, so I decided to do it myself. The results:

Standard PHPUnit output: meh.

Standard PHPUnit output

Disk defragmenter output: ❤️‍🔥

PHPUnit output with benholmen/defrag package

Give it a shot

Go ahead and install benholmen/defrag in your PHP project.

How does this work?

PHPUnit is the defacto test library in the PHP ecosystem and it's had a few flavors of "printers" and event systems. The current version is 10, and it uses event subscribers that allow you to register classes that can handle events that PHPUnit fires throughout the execution of the test suite.

Creating a PHPUnit extension

You create an extension that registers Subscribers for a variety of events, such as TestSuiteExecutionStarted and TestFailed. Each of these Subscribers calls a method on a DefragPrinter class that updates the UI depending on the outcome of the event. To properly draw the UI, I needed to know how many tests would be executed in the suite, and the outcome of each test (passed, failed, skipped, error, failed, etc) as they completed. The extension can also replace the standard progress output that PHPUnit provides by calling Facade@replaceProgressOutput. Finally, we can skip our fun defrag output under some conditions - user has requested quiet mode, TeamCity output, etc.

Drawing the UI: get ready to \e

I was very impressed with Jess Archer's Laravel Prompts package at Laracon US 2023, and chatted with her to see how I could leverage her work in this extension. I pulled in the Prompts package as a dependency so I could use a variety of methods to hide the cursor, move the cursor, and format text. I found the Prompts source code to be very readable and informative!

Drawing a terminal application is simple, and a little tedious. If you want to do more than write basic unformatted text, you'll need to look up a variety of ANSI escape codes and mark up your text with them. For example:

1\e[40m\e[31mE\e[39;49m

will write out an E with a red background and black text, then revert to the default formatting.

Code Purpose
\e[40m\e[31m Switch to red background, black foreground
E Print the E
\e[39;49m Revert to default background, default foreground

If you want to redraw over the same area, you have to move the cursor and print over it. You might be familiar with \r which will return the cursor to the beginning of the same line so you can continue updating a single line - it's useful for progress bars. In addition to that character, you can use control characters to move the cursor up and down lines or left and right within a single line, so you can update the entire screen.

I used this cursor movement to redraw lines that needed to change as the tests completed. There is a restriction to printing to the terminal - it's not instant - and I found that redrawing the entire display each time I needed to update a few characters resulted in a jumpy and flickering output. Theoretically you could update a single character, which would be fast, but with control sequences it's difficult to do a meaningful diff. I compromised and identified which lines had changed and only updated those lines. This saves about 70% of the redraw.

Limitations / wishlist

First, PHPUnit doesn't currently allow you to enable or disable an extension at runtime - it has to be enabled in phpunit.xml. I think that's clunky, and I really wanted to expose a vendor/bin/defrag command that you could run when you wanted fun output, and default to the boring output for editor integrations, CI/CD pipelines, etc.

Second, test suites can be run much faster with Paratest, a test package that spins up a number of PHPUnit processes to execute your test suite in parallel. Paratest wraps up PHPUnit and creates its own output, and does not expose an event system. I use Paratest regularly, so don't tell anyone, but I can't use my own package in most of my day to day coding.

Finally, Pest (which extends PHPUnit!) is rapidly growing in popularity and doesn't have a way to transform the output. For better and for worse, it's opinionated about what it prints out. I'd love to support Pest.

See also: the madman Joe Tannenbaum

About the time I was experimenting with this extension, Joe was cooking up some weird CLI experiments using Prompts. He's gone much further than I have, and best of all, wired up Charm Wish (a Go SSH package) with his experiments. Which means you can SSH into his experiments directly, like a BBS. It's incredible.

To view his resume, open up a terminal:

1ssh ssh.resume.joe.codes

Or view his experiments:

1ssh cli.lab.joe.codes

This blog is an experiment: less about education and more about documenting the oddities I've experienced while building web apps.

My hope is that you're here because of a random search and you're experiencing something I've had to deal with.

Popular Posts

Recent Posts

View all