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