Testing nested HTML Components with the test implementation
In the previous article, I settled on a solution to test data passed to the template renderer for an HTML component.
There's one caveat with the test implementation. The data of nested components gets json_encod
ed multiple times.
Let me demonstrate.
(As a reminder) we had these two interfaces:
interface Component
{
public function render(): string;
}
interface TemplateRenderer
{
public function render(string $templateName, array $data = []): string;
}
And the test implementation for the TemplateRenderer
is this:
class JsonEncodeTemplateRenderer implements TemplateRenderer
{
public function render(string $templateName, array $data = []): string
{
return json_encode($data);
}
}
(The TemplateRenderer
is passed as a dependency in the constructor of the Component
.)
If we have a component that has a child component, and that one has a child component, like so:
var_dump($component([
'foo' => 'foo',
'bar' => $component([
'baz' => 'baz',
'qux' => $component([
'quux' => 'quux',
]),
]),
]));
we end up with a JSON encoded string like this:
string(79) "{"foo":"foo","bar":"{\"baz\":\"baz\",\"qux\":\"{\\\"quux\\\":\\\"quux\\\"}\"}"}
It's not ideal because we used assertEquals
to compare arrays. If we apply the json_decode
as we did, we will get an array
that still contains JSON encoded values:
array(2) {
["foo"]=>
string(3) "foo"
["bar"]=>
string(41) "{"baz":"baz","qux":"{\"quux\":\"quux\"}"}"
}
The solution
I was never that thrilled with decoding the data in the tests anyway, so I created a "custom" assertion that does all the "heavy-lifting", and recursively decodes the string.
The "custom" assertion is as simple as it can be:
protected function assertTemplateRendererDataEquals(array $expected, string $actual, string $message = ''): void
{
$this->assertEquals(
$expected,
$this->decodeEncodedTemplateRendererData($actual),
$message
);
}
It just wraps the "native" assertEquals
and defers the work to a private method:
private function decodeEncodedTemplateRendererData(mixed $data): mixed
{
if (!is_string($data) && !is_array($data)) {
return $data;
}
if (is_string($data)) {
try {
$data = json_decode($data, true, flags: JSON_THROW_ON_ERROR);
} catch (\Exception) {
return $data;
}
}
if (is_array($data)) {
foreach ($data as $key => $value) {
$data[$key] = $this->decodeEncodedTemplateRendererData($value);
}
}
return $data;
}
And with all this in place, in the tests, I can simply do this and forget about the complexity:
$this->assertTemplateRendererDataEquals(
[
'foo' => 'foo',
'bar' => [
'baz' => ...
]
],
$component->render()
);
The code to create those components "on-the-fly":
$component = fn(array $data = []): string => (new class(
new JsonEncodeTemplateRenderer(),
$data
) implements Component {
public function __construct(
readonly private TemplateRenderer $templateRenderer,
readonly private array $data,
) {
}
public function render(): string
{
return $this->templateRenderer->render(
'index.php',
$this->data,
);
}
})->render();