サーバサイド(CakePHP)で生成するHTML検証ツール作成した フロントエンド定例 2022/11/11

igarashisho|2022年11月11日
フロントエンド

こんにちは、フロントエンドチームの @syo_igarashi です。
今週のフロントエンド定例の内容を記載します。

フロントエンド定例について、以前の記事(ランサーズのフロントエンドチームが取り組んでいること)でお伝えしたのですが、毎週金曜日に開催しており、実際の業務で取り組んでいることや気になった技術情報等をシェアしあう会になっています。

以下、今週の内容です。

サーバサイド(CakePHP)で生成するHTML検証ツール作成した

CakePHPのテンプレートエンジン経由で生成されるHTMLの確認方法がデータベースのデータとコントローラーの挙動依存で確認していたのが大変だと思ったので対象のctpのみにフォーカスした検証ツールを作成しました。

React側でStorybook駆動にやっていきましょうと言ってて昔からあるHTMLの生成に関してはそっ閉じしてたとこもあったんで向き合ってみたというのもあります。不本意ですけど。

方法としてPHPUnitでctpのテストファイルを作成し、ctpに必要な引数をモックで作成して検証するようなものを作りました。PHPUnitのテストファイルでctpのrender結果をreturnさせてツール側でreturnの内容を描画させるようなやり方で実現させました。

以下スクショで対象のテストのあるディレクトリでファイル検索し、PHPUnitの関数のリンクを生成して実行するようにしています。

以下はソースになります。テンプレートエンジンまで検証しようとするの今までの経験上なかった気がするので参考になるんじゃないかなと思います。下記はCakePHP 2の想定のコードですが考え方は他のFWでも似たような感じで行けると思います。(テスト関数内でビューの実行ができるか、テストフレームワークの関数で項目の取得しやすさの違いはあるかもしれませんが

ところどころパスとかは隠してるので各自のプロジェクトに合わせて変更してください(伝われ

TestCTPCheckToolController

<?php

/**
 * ctpファイルに対してのテストファイル経由にHTMLの描画を確認するツールです
 */
class TestCTPCheckToolController extends Controller
{
    public $layout = null;

    public function beforeFilter()
    {
        parent::beforeFilter();

        if (! 本番か開発環境か識別する関数を用いる ) {
            throw new NotFoundException();
        }
    }

    /**
     * Action method
     * ctpのテストファイル一覧
     *
     * @access public
     */
    public function index()
    {
        $viewElementFiles = $this->getFileList(APP . 'Test/Case/View/CTP');
        $this->set('viewElementFiles', $viewElementFiles);
    }

    /**
     * Action method
     * ctpのテストファイルにある関数一覧
     *
     * @access public
     */
    public function functions()
    {
        $filePath = $this->request->query('file');

        if (empty($filePath) || !file_exists($filePath)) {
            return $this->redirect(' TestCTPCheckToolController::indexにリダイレクトさせる ');
        }

        $testClassName = $this->getClassName($filePath);

        $testRunner = new class() extends PHPUnit_Runner_BaseTestRunner {
            protected function runFailed($message)
            {
                exit(1);
            }
        };
        /**
         * @var PHPUnit_Framework_TestSuite $testSuite
         */
        $testSuite = $testRunner->getTest($testClassName, $filePath);
        $testFunctions = $testSuite->tests();

        $testFunctionNames = [];
        foreach ($testFunctions as $testFunction) {
            array_push($testFunctionNames, $testFunction->getName());
        }

        $this->set('filePath', $filePath);
        $this->set('testFunctionNames', $testFunctionNames);
    }

    /**
     * Action method
     * ctpのテストファイルにある関数から描画の確認をする
     *
     * @access public
     */
    public function check()
    {
        $filePath = $this->request->query('file');

        if (empty($filePath) || !file_exists($filePath)) {
            return $this->redirect(' TestCTPCheckToolController::indexにリダイレクトさせる ');
        }

        $functionName = $this->request->query('function');

        if (empty($functionName)) {
            return $this->redirect(' TestCTPCheckToolController::indexにリダイレクトさせる ');
        }

        $testClassName = $this->getClassName($filePath);

        $testRunner = new class() extends PHPUnit_Runner_BaseTestRunner {
            protected function runFailed($message)
            {
                exit(1);
            }
        };
        /**
         * @var PHPUnit_Framework_TestSuite $testSuite
         */
        $testSuite = $testRunner->getTest($testClassName, $filePath);
        $testFunctions = $testSuite->tests();

        $ctpString = '';
        foreach ($testFunctions as $testFunction) {
            if ($testFunction->getName() === $functionName) {
                $ctpString = $testFunction->$functionName();
            }
        }

        $this->set('ctpString', $ctpString);
        $this->set('filePath', $filePath);
        $this->set('functionName', $functionName);
    }

    /**
     * @param string $dir
     * @return string[]
     */
    private function getFileList(string $dir): array
    {
        $files = glob(rtrim($dir, '/') . '/*');
        $list = [];

        foreach ($files as $file) {
            if (is_file($file)) {
                $list[] = $file;
            }
            if (is_dir($file)) {
                $list = array_merge($list, $this->getFileList($file));
            }
        }
        return $list;
    }

    /**
     * @param string $filePath
     * @return string
     */
    private function getClassName(string $filePath): string
    {
        preg_match('/\w*\.php/', $filePath, $fileMatch);
        $className = preg_replace('/\.php/', '', $fileMatch[0]);

        return $className;
    }
}

それぞれのAction methodに対応したctpも記載します。

index.ctp

<?php
/**
 * @var string[] $viewElementFiles
 */
?>


<div>
    <h1>CTPCheckTool</h1>
    <h2>CTPのテストファイル一覧</h2>


    <div>
        <h3>ルール・使い方</h3>
        <ol>
            <li>
                Modelを使用する処理がある箇所はMockeryでやるようにお願いします。
            </li>
            <li>
                テストの関数のreturnで宣言した文字列が描画されるので<strong>$view->render('ctp指定', null);</strong>をreturnするようにお願いします。
            </li>
            <li>
                テスト範囲は<strong>??????/Test/Case/View/CTP/*</strong>にしています。
            </li>
            <li>
                CSSの反映は極力対象のctp内に閉じるようにしましょう。(共通利用されているものはcheck.ctp側でCSS読み込みしてもいいかも)
            </li>
        </ol>
    </div>

   <hr></hr>

   <ul>
       <?php foreach ($viewElementFiles as $viewElementFile): ?>
           <li>
               <a href=" TestCTPCheckToolController /functions/?file=<?= h($viewElementFile) ?>">
                   <?= h($viewElementFile) ?>
               </a>
          </li>
       <?php endforeach; ?>
   </ul>
</div>

functions.ctp

<?php
/**
 * @var string $filePath
 * @var string[] $testFunctionNames
 */
?>

<div>
    <h1>CTPCheckTool</h1>
    <h2>CTPのテストファイルの関数一覧</h2>
    <h3><?= h($filePath) ?></h3>

    <hr></hr>

    <ul>
        <?php foreach ($testFunctionNames as $testFunctionName): ?>
            <li>
                <a href=" TestCTPCheckToolController /check/?file=<?= h($filePath) ?>&function=<?= h($testFunctionName) ?>">
                    <?= h($testFunctionName) ?>
                </a>
            </li>
        <?php endforeach; ?>
    </ul>
</div>

check.ctp

<?php
/**
 * @var string $ctpString
 * @var string $filePath
 * @var string $functionName
 */
?>

<?php
/**
 * 共通利用されるCSS, JavaScriptの記載とかここにあってもいいかもしれませんね
 */
?>

<div>
    <h1>CTPCheckTool</h1>
    <h2><?= h($filePath) ?></h2>
    <h3><?= h($functionName) ?></h3>

    <hr></hr>

    <div>
       <?= $ctpString ?>
    </div>
</div>

ビュー自体のテストをどう書いてるかという疑問についてCakePHP 2想定になりますが

$view = new \View();
$view->viewVars = [
    'user' => $userMock,
    'hoge' => 'hoge',
];
$view->layout = null;
$result = $view->render('対象のctp', null);

$this->assertRegExp('/div/', $result);

return $result;

みたいな感じでassertRegExpでビューに引数の内容でif文の分岐で出力されたHTMLに対してassertかけながらreturnにHTML内容を返すような感じにしてユニットテストを書きながら今回作成したツールにも対応するように書いてます。

 

前回の定例内容はこちらから確認可能ですのでご興味いただければ下記のリンクから閲覧いただければと思います。

https://engineer.blog.lancers.jp/?s=フロントエンド定例