Testing HTML Components
For a WordPress (classic) theme using get_template_part
generally provides a good level of organization. But for a large, enterprise-level website, I organize my server-side rendered UI elements into what I call "HTML Components". It's somewhat inspired by React.
Components let you split the UI into independent, reusable pieces, and think about each piece in isolation.
Since we prefer OOP for this project, there's an interface for the components:
interface Component
{
public function render(): string;
}
A component could be as simple as this one, but typically there's more to it:
class Infobox implements Component
{
public function render() {
return '<div class="infobox">...</div>';
}
}
There's a template renderer to avoid mixin HTML in the PHP class and achieve a better separation of concerns:
interface TemplateRenderer
{
public function render(string $templateName, array $data = []): string;
}
Besides passing the data to the template files, components can retrieve icons, derive state from props (React jargon), etc., do what's needed.
Here's a stripped-down but "real" component:
class Component {
use ResourcesDirTemplateFileTrait;
public function __construct(
readonly private Props $props,
readonly private TemplateRenderer $templateRenderer,
readonly private Svg\Loader $svgLoader
) {
}
public function render(): string
{
return $this->templateRenderer->render(
$this->templateFile(),
[
TemplateArgs::TITLE => $this->props->title,
TemplateArgs::CONTENT => $this->props->content,
TemplateArgs::DEFAULT_IS_OPEN => json_encode(!$this->props->title),
TemplateArgs::ICON_TOGGLE => $this->props->title
? $this->svgLoader->byName('arrow-caret-down')
: '',
]
);
}
}
While these components do not contain "too much logic", I strive to provide tests for them.
Testing
When testing these components, I'm interested in two things. First, if the components are rendering the output (HTML) I expect. Second, if the data passed to the template files is what it should be.
Testing the output also tests the data indirectly. There's an overlap between the two types of tests, but they can't be exchanged, and they focus on different aspects.
I made the rule to always write tests "for the HTML", but only test the data directly for more "complex" components.
"Snapshot" testing
PHPUnit had the assertEqualXMLStructure
method, which made it easy to compare two DOM structures. There are plans to reintroduce it.
Until then, I'm comparing two "HTML" strings. This has its shortcomings. For example, the order of the attributes has to match; spacing can cause problems, etc.
// This fails
$this->assertEquals(
'<div class="infobox" id="infobox-alpha"></div>',
$infobox->render() // <div class="infobox-alpha" class="infobox">...</div>
);
To overcome this, I have a custom assertion method that does some normalization before comparing the strings:
class InfoboxHtmlSnapshotTest extends HtmlSnapshotTestCase
{
public function testDefaultOutput()
{
$infobox = new Infobox(...);
$this->assertHtmlEquals(
file_get_contents(__DIR__ . '/snapshots/default.html'),
$infobox->render()
);
}
}
I'm doing something similar to what Jest calls snapshot testing but the "snapshots", in my case, are created and maintained manually.
Testing the $data
with mocking
My initial approach for testing the passed data to the template renderer was mocking it and setting up expectations.
public function testNoIconAndIsOpenWhenNoTitle()
{
$templateRendererMock = $this->createMock(TemplateRenderer::class);
$templateRendererMock->expects($this->once())
->method('render')
->with(
$this->stringContains('index.php'),
[
'title' => '',
'content' => 'In 2009, the International Air Transport Association (IATA) ...',
'initialIsOpen' => 'true',
'iconToggle' => '',
]
);
$infobox = new Infobox(
...,
$templateRendererMock,
...
);
$infobox->render();
}
This approach started to be messy when I created components that rendered child components.
In those cases, the render
method was called multiple times. Switching and using the withConsecutive
turned out to be hard to reason about and follow.
Using a test implementation
The contract of the TemplateRenderer
had to be satisfied, so there was no way around returning something else than a string
.
But the option to create a test implementation for the template renderer was open. So that's what I did.
$customImplTemplateRenderer = new class implements TemplateRenderer
{
public function render(string $templateName, array $data = []): string;
{
return json_encode($data);
}
};
This implementation simply returns the data in a JSON encoded string format, which, once decoded, allows for good old array comparison:
$infobox = new Infobox(
...,
$customImplTemplateRenderer,
...
);
$this->assertEquals(
[
'title' => '',
'content' => 'In 2009, the International Air Transport Association (IATA) ...',
'initialIsOpen' => 'true',
'iconToggle' => '',
],
json_decode($infobox->render(), true)
);
When nesting HTML Components, it's just a matter of comparing a multidimensional array, and there's no need for writing the boilerplate for creating a mock.