VSCode で PHPUnit を実行する拡張を使ってみた

VSCode の Marketplace で PHPUnit で検索して出てきた中からダウンロード数が多い 3 つの拡張を使ってみました。

なお、普段は↓のようなめんどくさい環境でコードを書いています。

  • Windows で PhpStorm を実行している
  • PHP は WSL 上の docker-compse コマンドを使って Docker 環境で実行している
  • direnv で環境変数をいろいろ設定している

ので同様に VSCode も Windows 上から WSL 上で direnv を適用しつつ docker-compse を使いたいです。

emallin.phpunit

デフォで vendor/bin/phpunit とか vendor/phpunit/phpunit/phpunit とか phpunit*.phar とかを再帰的に検索して php コマンドで実行してくれます。vendor/bin/phpunit 決め打ちじゃないのと Windows の場合に vendor/phpunit/phpunit/phpunit を探してくれるのは便利です。Phar もプロジェクトディレクトリ内に置いとけば自動で見つけてくれます。

リモート実行する場合、次のように任意のコマンドが指定できるので、Windows 上から WSL の docker-compose を実行すれば OK です。テストファイルを指定して実行するときのために phpunit.paths も指定する必要があります。

{
    "phpunit.command": "wsl direnv exec . docker-compose run --rm app",
    "phpunit.phpunit": "vendor/bin/phpunit",
    "phpunit.paths": {
        "${workspaceFolder}": "/app",
    }
}

SSH や Docker で実行するための別の設定 phpunit.ssh とか phpunit.docker.container とか docker.image とかもありますが、phpunit.command だけで指定するほうが簡単なんじゃないかと思います。

メソッドを指定して実行するときにメソッド名が単純に --filter に渡されるので、例えば hoge というメソッドのテストを実行するときに hogehoge も一緒に実行されてしまいます。変に頑張られると Windows と Linux でのシェルのクオートやエスケープの違いでうまく動かなくなったりするので、これでも十分かなと言う気もします。

phpunit.args 設定で PHPUnit のコマンドラインオプションを配列で指定できます。配列なので適切なエスケープが施されるか、あるいはシェルを経由しないのかと思いきや、実際には単純に join でスペース区切りで繋げられてシェル経由で実行されます。これ配列である必要全然無いような。。。むしろ phpunit.command phpunit.phpunit phpunit.args をまとめて1つの文字列の設定でも十分な気もします。実際のところ次のように適当にバラけさせても動きます。

{
    "phpunit.command": "wsl direnv",
    "phpunit.phpunit": "exec .",
    "phpunit.args": ["docker-compose", "run", "--rm", "app", "vendor/bin/phpunit"],
    "phpunit.paths": {
        "${workspaceFolder}": "/app",
    },
}

リモート実行のために phpunit.paths でパスのマッピングをしているとき、テストがコケたときに赤線がついたり問題タブに表示されたりするのが機能しなくなることがあります。下記で /app がハードコードされているためなので Docker 内では /app にソースを配置すれば大丈夫です。

PHPUnit は拡張が直接実行しているわけではなく、VSCode の ShellExecution タスクとして実行するようになっています。problemMatchers という VSCode の拡張ポイントを使って実行結果から正規表現でコケたテストケースのファイルや行番号やメッセージを抜き出すようになっているようです。この problemMatchers は動的にはできなさそうなのでこのような動作になっているようです。

calebporzio.better-phpunit

デフォだと vendor/bin/phpunit が(Windows なら vendor/bin/phpunit.bat)が決め打ちで実行されます。 better-phpunit.phpunitBinary でコマンドは変更できます。これはシェルで解釈されるので Windows なら php vendor/phpunit/phpunit/phpunit とかでも OK です。

Docker などでリモート実行するときは次のようにコマンドを指定します。ローカルとリモートのファイル名のマッピングで ${workspaceFolder} は使用できないので次のように絶対パスで指定する必要があります。なお、Windows でもパス区切りは \\ ではなく / で指定します。

{
    "better-phpunit.phpunitBinary": "vendor/bin/phpunit",
    "better-phpunit.docker.enable": true,
    "better-phpunit.docker.command": "wsl direnv exec . docker-compose run --rm app",
    "better-phpunit.docker.paths": {
        "c:/Users/oreore/path/to/project": "/app"
    },
}

better-phpunit.docker.commandbetter-phpunit.phpunitBinary は単にスペース区切りで連結されるだけです。なので次のように適当にバラしても通ります。

{
    "better-phpunit.docker.enable": true,
    "better-phpunit.docker.command": "wsl direnv exec .",
    "better-phpunit.phpunitBinary": "docker-compose run --rm app vendor/bin/phpunit",
    "better-phpunit.docker.paths": {
        "c:/Users/oreore/path/to/project": "/app"
    },
}

設定の項目名に docker と付いていますが Docker でなくても動きます。例えば次のようにすれば Windows 上の VSCode から WSL 上の PHPUnit を実行できます。

{
    "better-phpunit.docker.enable": true,
    "better-phpunit.docker.command": "wsl php",
    "better-phpunit.phpunitBinary": "vendor/bin/phpunit",
    "better-phpunit.docker.paths": {
        "c:/Users": "/c/Users"
    },
}

次のようにすればローカルの Windows 上の PHP が実行されます。つまり最初の better-phpunit.phpunitBinary だけ指定したときと同じです。

{
    "better-phpunit.docker.enable": true,
    "better-phpunit.docker.command": "php",
    "better-phpunit.phpunitBinary": "vendor/phpunit/phpunit/phpunit",
}

コケたテストに赤線をつけたり問題タブに表示したりする機能が機能していないようです。たぶん package.jsonproblemMatchers のパターンが間違っているためだと思います。また、仮に problemMatchers が修正されたとしても「リモートの実行結果のパス → ローカルのパス」のマッピングはサポートしてなさそうなので、リモート実行だと赤線や問題タブには表示されません。

なお、emallin.phpunit と同様で PHPUnit は拡張が直接実行しているわけではなく、VSCode の ShellExecution タスクとして実行するようになっています。problemMatchers という拡張ポイントを使って実行結果から正規表現でコケたテストケースのファイルや行番号やメッセージを抜き出すようになっているようです。

recca0120.vscode-phpunit

サイドバーにテストのエクスプローラーが表示できたり、CodeLenses でコード上からサクッと実行できたり、多機能です。

前述の 2 つと比べるとだいぶ複雑です。内部では LanguageServer を実行していて PHPUnit は LanguageServer から child_process.spawn で直接実行されています。コケたテストのファイル名や行番号やメッセージは PHPUnit で出力した JUnit XML から抜き出しているようです。

素のままだとデフォで vendor/bin/phpunit (Windows なら vendor/bin/phpunit.bat)を実行します。設定の phpunit.phpunit で変更できますが、絶対パスで指定する必要があります。相対パスで指定しても頭に / が付けられてしまいます。

phpunit.php で PHP のバイナリを指定すれば PHPUnit のコマンドを直接ではなく明示的に PHP コマンドで実行されるようになります。Windows で Phar を指定するときに使えそうです。なお、これも絶対パスで指定する必要があります。パスが通ったところにあるコマンドを指定しても頭に / が付けられるのでダメです。

phpunit.args で PHPUnit のコマンドラインオプションが指定できます。phpunit.args-c が含まれないときは phpunit.xml.dist または phpunit.xml が検索され、見つかればコマンドラインに自動で追加されます。これも絶対パスで追加されます。

テストファイルを指定して実行するとき、phpunit.relativeFilePath: true に設定されていると相対パスで指定されるようになります。リモート実行するときはローカルの絶対パスが指定されてもダメなのでこのオプションが必須です。ただし前述の phpunit.xml が自動で追加されるときは絶対パスのままなので、リモート実行させるなら phpunit.args-c オプションを指定する必要があります。

Windows で実行するとき、パス区切り文字はバックスラッシュのままです。なので Windows から Docker などでリモート実行させるのは難しいです。

phpunit.shell でシェルを経由するかどうかが指定できます。これはそのまま child_process.spawn のオプションになります。

カーソル位置のテストを実行や、テストエクスプローラーや CodeLenses で単一のメソッドのテストを実行する機能が機能していません。たぶん下記の問題だと思いますが・・

WSL 上で VSCode を実行しているならこれで解決します。

{
    "phpunit.shell": "bash"
}

Windows 上で VSCode を実行しているときはどうしようもありません。不便なので治しておきました。

もうマージされてリリースされているようなのでこの問題は解決していると思います。

さいごに

recca0120.vscode-phpunit が高機能で IDE っぽい感じで良さそうですが、Windows 上の VSCode からのリモート実行が絶望的です。Remote Development とかで Docker 内で VSCode を動かせばいいんでしょうけど。

emallin.phpunit は Docker 内で /app にコードを置く必要がある、という問題はありますが・・それさえ目をつぶれば一番安定して使えそうです。

calebporzio.better-phpunit はコケたテストの赤線や問題タブが機能しないので微妙です。それ以外は動作原理は emallin.phpunit とほぼほぼ同じで違いがあまりありません。