PHP_CodeSnifferのカスタムルールのテストを書くよ

2023/07/25に公開

はじめに

PHP_CodeSnifferのカスタムルールの作り方は公式チュートリアルに書いてあるけど、テストコードに関する情報が無いので実際に作った内容を記事に書いておこうと思います。

ちなみに、PHP_CodeSnifferには元々PHPUnitでテストを書く仕組みは用意されています。
PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTestなど

ただ、composer.jsonを見る限り、対応しているPHPUnitのバージョンも古いので今回は独自にテストを作成したいと思います。
https://github.com/squizlabs/PHP_CodeSniffer/blob/3.7.2/composer.json#L35

環境

  • PHP 8.2.7
  • PHP_CodeSniffer 3.7.2
  • PHPUnit 10.2.2

今回は以前作ったカスタムルールの変数名はスネークケース以外はダメってルールのテストを作成しようと思います。
https://github.com/naopusyu/phpcs-custom-rules

テストクラスの作成

testsディレクトリ配下の任意のところに作ってもらえれば問題ないと思いますが、
今回はSniffファイルと同じディレクトリ構成にしておこうと思います。

src/PHPCSCustomRules/Sniffs/NamingConventions/SnakeCaseVariableNameSniff.php
↓
tests/PHPCSCustomRules/Sniffs/NamingConventions/SnakeCaseVariableNameSniffTest.php

テストケースの作成

テストケースに必要な内容としては、次の通りです。

  1. テストファイルの作成
  2. PHP_CodeSniffer\Configのインスタンス作成
  3. PHP_CodeSniffer\Rulesetのインスタンス作成
  4. PHP_CodeSniffer\Files\LocalFileのインスタンス作成
  5. Sniffファイル実行結果の検証
  6. 全てを組み合わせる

1. テストファイルの作成

テストではPHP_CodeSnifferを動かして、エラーが起きる、起きないの検証を行うので、適当な内容のphpファイルを用意します。
作成する場所はテストクラスと同じ場所にしておくと、どのテストのテストファイルなのかわかりやすくて良いかと思います。

tests/PHPCSCustomRules/Sniffs/NamingConventions/fixture.php

今回は変数名が、スネークケースなのかを検証するルールのテストなので、下記のような内容にしておきます。

tests/PHPCSCustomRules/Sniffs/NamingConventions/fixture.php
<?php

$snake_case = 1;
$camelCase = 2;

class A 
{
    public int $property_name = 1;
    public int $propertyName = 2;

    public function methodA(int $variable_name)
    {
        $snake_case = 1;
        $camelCase = 2;
    }

    public function methodB(int $variableName)
    {
        $snake_case = 1;
        $camelCase = 2;
    }
}

2. PHP_CodeSniffer\Configのインスタンス

PHP_CodeSniffer\Configはインスタンスを生成するだけで他は特に何もしないです。


use PHP_CodeSniffer\Config;

$config = new Config();

3. PHP_CodeSniffer\Rulesetのインスタンス

PHP_CodeSniffer\Rulesetは実行したいSniffファイルをロードするためのクラスです。
インスタンスを生成するには、PHP_CodeSniffer\Configのインスタンスが必要になります。

また、registerSniffsメソッドを使ってテストで実行したいSniffファイルの読み込みを行います。


use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Ruleset;

$config = new Config();
$ruleset = new Ruleset($config);

// 指定するときは必ず配列で指定してください。
$sniffFiles = [__DIR__ . '/../../../../src/PHPCSCustomRules/Sniffs/NamingConventions/SnakeCaseVariableNameSniff.php'];

// 2,3引数はテスト実施時は空配列で問題ないです。
$ruleset->registerSniffs($sniffFiles, [], []);
$ruleset->populateTokenListeners();

populateTokenListenersメソッドは指定したSniffファイルを実行するために呼んでおく必要があるそうです。

4. PHP_CodeSniffer\Files\LocalFileのインスタンス

1 ~ 3の内容を組みあせて、PHP_CodeSniffer\Files\LocalFileのインスタンスを生成します。
processメソッドを呼び出せば指定したSniffファイルを実行することができます。


use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\LocalFile;
use PHP_CodeSniffer\Ruleset;

$fixtureFile = [__DIR__ . '/tests/PHPCSCustomRules/Sniffs/NamingConventions/fixture.php']

$config = new Config();
$ruleset = new Ruleset($config);

~~~~~ 省略 ~~~~~

$phpcsFile = new LocalFile($fixtureFile, $ruleset, $config);

$phpcsFile->process();
$foundErrors = $phpcsFile->getErrors();

getWarningsgetErrorsメソッドを使うことで実行結果を次のような連想配列で取得することが可能です。

最初の階層のキーがエラーが発生している行番号になります。

Array
(
  [4] => Array
  (
    [1] => Array
    (
      [0] => Array
      (
        [message] => Variable name "camelCase" is not in snake_case format
        [source] => PHPCSCustomRules.NamingConventions.SnakeCaseVariableName.found
        [listener] => Naopusyu\PHPCSCustomRules\Sniffs\NamingConventions\SnakeCaseVariableNameSniff
        [severity] => 5
        [fixable] => 
      )
    )
  )
)

5. Sniffファイル実行結果の検証

今回は想定している箇所(行)を検出できれば良いかと思いますので、Sniffファイル実行後に取得できる行番号が正しいかを検証したいと思います。

1. テストファイルの作成の内容では、4, 9, 14, 17, 20行目がエラーになるはずです。

$foundErrors = $phpcsFile->getErrors();
$lines = array_keys($foundErrors);
$this->assertSame([4, 9, 14, 17, 20], $lines);

6. 全てを組み合わせる

ここまでの内容を組み合わせると次のようなテストケースになります。

tests/PHPCSCustomRules/Sniffs/NamingConventions/SnakeCaseVariableNameSniffTest.php
<?php

declare(strict_types=1);

namespace Naopusyu\PHPCSCustomRules\Tests\Sniffs\NamingConventions;

use PHPUnit\Framework\TestCase;
use PHP_CodeSniffer\Files\LocalFile;
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Config;

class SnakeCaseVariableNameSniffTest extends TestCase
{
    public function test(): void
    {
        $fixtureFile = __DIR__ . '/fixture.php';
        $sniffFiles = [__DIR__ . '/../../../../src/PHPCSCustomRules/Sniffs/NamingConventions/SnakeCaseVariableNameSniff.php'];
        $config = new Config();
        $ruleset = new Ruleset($config);
        $ruleset->registerSniffs($sniffFiles, [], []);
        $ruleset->populateTokenListeners();
        $phpcsFile = new LocalFile($fixtureFile, $ruleset, $config);
        $phpcsFile->process();
        $foundErrors = $phpcsFile->getErrors();
        $lines = array_keys($foundErrors);
        $this->assertSame([4, 9, 14, 17, 20], $lines);
    }
}

テストの実行

事前にbootstrapファイルを用意して、テストクラス内でPHP_CodeSnifferを動かすためにPHP_CodeSnifferのtests/bootstrap.phpを読み込んでおきます。

tests/bootstrap.php
<?php

require_once __DIR__ . '/../vendor/squizlabs/php_codesniffer/tests/bootstrap.php';

テスト実行はPHPUnitと同じです。

$ ./vendor/bin/phpunit --bootstrap ./tests/bootstrap.php ./tests/PHPCSCustomRules/Sniffs/NamingConventions/SnakeCaseVariableNameSniffTest.php
PHPUnit 10.2.2 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.2.7

.                                                                   1 / 1 (100%)

Time: 00:00.017, Memory: 10.00 MB

OK (1 test, 1 assertion)

まとめ

PHP_CodeSnifferのカスタムルールのテストケースを作成しました。

今回は単純にエラーが発生した行番号の検証だけなので、プロダクトコードを直接検証すればいいみたいな話かもしれないですが、パッケージとして公開している場合はユニットテストがあった方が良いかと思います。

カスタムルールの日本語ドキュメントが少ないのでこれが誰か(n年後の自分)の役に立つことを祈っております。

Discussion