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