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_encoded 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();