PHPUnit
dom
crawler

PHPUnitでHTMLをDOMでパースしてテスト

PHPでなんらかのHTMLを出力するコードをテストする際に、これまでは正規表現とかでいい加減なチェックをしてたんですが、割と大きめなHTMLを出す際にいい加減不安なのでDOMを使ってチェックすることにしました。

当初はPHP本体の DOMDocument::loadHTML でやろうとしたんですが、HTML5
<section> を使うと DOMDocument::loadHTML(): Tag section invalid in Entity, line: 1 と怒られてしまって断念。

その後いろいろ試した結果以下の2つのライブラリを使用すれば CSS セレクタでいい感じにチェックできました。

  • symfony/dom-crawler
  • symfony/css-selector

いつも思うんですが、Symfony 先輩さすがですね。

依存関係のインストール

まず、上の2つを composer でインストール。

$ composer require symfony/dom-crawler symfony/css-selector --dev

今回はテストで使うので --dev 付きで。

DOMを返すメソッドをテストケースに書く

まず、HTML を渡すと DOM を返してくれるメソッドを用意します。<html></html> とかで囲ってあげなくてもいいみたい。

<?php
class Sample_Test extends UnitTestCase
{
    private function dom( $html )
    {
        $dom = new Symfony\Component\DomCrawler\Crawler();
        $dom->addHTMLContent( $html, "UTF-8" );
        return $dom;
    }
}

テストを書く

次に実際のテストを書きます。test_html() というメソッドがそれ。
do_something()は何らかのHTMLを返す関数です。

<?php
class Sample_Test extends UnitTestCase
{
    public function test_html()
    {
        $result = do_something();
        $dom = $this->dom( $result );

        $this->assertSame( 1, count( $dom->filter( '.col-3' ) ) );
        $this->assertSame( 6, count( $dom->filter( 'section.item' ) ) );
    }

    private function dom( $html )
    {
        $dom = new Symfony\Component\DomCrawler\Crawler();
        $dom->addHTMLContent( $html, "UTF-8" );
        return $dom;
    }
}

この例では、CSSセレクタを使用して、そのセレクタにマッチする要素が期待した数だけあるかをチェックしています。

$this->assertSame( 1, count( $dom->filter( '.col-3' ) ) );
$this->assertSame( 6, count( $dom->filter( 'section.item' ) ) );

ドキュメントは以下。

https://symfony.com/doc/current/components/dom_crawler.html

このテストでできないこと。

HTMLの構文チェックはできません。たとえば閉じタグがないタグを置いても頑張ってパースしてくれてしまいます。
そして <hoge> みたいに適当な要素をおいても同じく頑張ってくれてしまいます。

以下のようなメソッドで閉じタグ漏れぐらいはチェックできるかも。

private function assertMaybeValidHTML( $html )
{
    $html = str_replace( 'section', 'div', $html );
    $html = str_replace( 'aside', 'div', $html );
    $dom = new DOMDocument();
    $dom->loadHTML( '<html><body>' . $html . '</body></html>' );
}