A Better Way to Test HTML in WordPress with assertEqualHTML()

WordPress 6.9 just introduced assertEqualHTML(), and honestly, it’s about time. If you’ve written PHPUnit tests for WordPress plugins that produce HTML output—like block render callbacks or formatting filters—you know the specific flavor of hell that is a fragile string assertion. You spend an hour getting a test to pass locally, only for it to fail on CI because an attribute came back in a different order or you added a trailing semicolon to an inline style. The browser renders the exact same thing, but your test suite acts like the world is ending.

As someone who has been unit testing WordPress code for over a decade, I’ve seen countless “hacks” to get around this, from regex nightmares to heavy DOMDocument comparisons. assertEqualHTML(), available on WP_UnitTestCase, finally treats HTML as a semantic tree rather than just a raw string.

The problem with assertSame() for HTML

Consider a simple filter that uses the HTML API to add loading="lazy" to images. In a traditional test, you might do something like this:

public function test_bbioon_lazy_load_adds_attribute(): void {
    $input    = '<p><img src="photo.jpg" alt="A photo"></p>';
    $expected = '<p><img loading="lazy" src="photo.jpg" alt="A photo"></p>';

    $this->assertSame( $expected, bbioon_lazy_load_images( $input ) );
}

This test is incredibly brittle. If a future version of WordPress changes the serialization order—putting src before loading—this test fails. Consequently, you end up maintaining tests that don’t actually catch bugs; they just catch non-breaking changes in how the parser serializes data. Furthermore, refactoring your input to use double quotes instead of single quotes would break every assertSame() in your suite.

How assertEqualHTML() Normalizes Markup

Specifically, assertEqualHTML() works by parsing both strings into a normalized tree using WP_HTML_Processor. Two strings that produce the same semantic tree are treated as equal. Therefore, the order of attributes, class names, or whitespace in styles no longer triggers a failure.

What differsassertSame()assertEqualHTML()
Attribute orderFailsPasses
Class name orderFailsPasses
Tag capitalizationFailsPasses
Character references (&not; vs ¬)FailsPasses
Different attribute valuesCatchesCatches

Testing Block Render Callbacks

Dynamic blocks often produce complex, deeply nested markup. Using assertEqualHTML() here is a game changer. You can write your expected HTML using NOWDOC syntax to keep it readable without worrying about how WP_HTML_Tag_Processor decides to order the classes you’ve injected.

public function test_bbioon_card_block_output(): void {
    $attributes = array(
        'backgroundColor' => '#f5f5f5',
        'className'       => 'custom-card my-theme-class',
    );
    $content = '<p>Content</p>';
    $output  = bbioon_render_card_block( $attributes, $content );

    $expected = <<<'HTML'
<div
    class="my-theme-class custom-card wp-block-bbioon-card"
    style="background-color: #f5f5f5;"
>
    <p>Content</p>
</div>
HTML;

    $this->assertEqualHTML( $expected, $output );
}

In this example, assertEqualHTML() ignores the fact that my-theme-class appears before custom-card in the expected string but might be swapped in the actual output. It also normalizes the trailing semicolon in the style attribute.

Understanding Failure Tree Output

When this assertion fails, you don’t just get a messy string diff. Instead, it outputs a normalized tree representation. For block developers, this is huge because it understands block delimiters and renders them as BLOCK["namespace/name"] with formatted JSON attributes. This allows you to quickly pinpoint exactly which element or block attribute is causing the mismatch.

However, don’t throw assertSame() away entirely. If you are testing a function where the literal string output matters—like a code generator or a function feeding a specific legacy parser—then assertSame() is still the correct choice. For everything else that eventually hits a browser, the semantic approach is superior.

If you haven’t yet, you should definitely set up a CI pipeline that utilizes these modern assertions. It saves an incredible amount of time in the long run.

Look, if this assertEqualHTML() stuff is eating up your dev hours, let me handle it. I’ve been wrestling with WordPress since the 4.x days.

Pragmatic Takeaway

Stop fighting the serializer. If you are building plugins for WordPress 6.9 and beyond, migrate your HTML assertions to assertEqualHTML(). It makes your tests more resilient to core updates and parser changes, letting you focus on testing logic rather than string positions. For more technical details, check out the WP_HTML_Tag_Processor documentation or the original TRAC ticket #63527.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.

Leave a Comment