「PHP tool」の版間の差分
(PHPDoc) |
(Methods @throws) |
||
| (同じ利用者による、間の118版が非表示) | |||
| 5行目: | 5行目: | ||
PHPDocはPHP流のコメントのスタイル。phpDocumentorはPHPDocからドキュメントを生成するツール。PHPDocを解析して文書を作成したり利用するソフトはいろいろある。例えば、VS CodeやPHPStormなどのIDEもPHPDocを使う。 | PHPDocはPHP流のコメントのスタイル。phpDocumentorはPHPDocからドキュメントを生成するツール。PHPDocを解析して文書を作成したり利用するソフトはいろいろある。例えば、VS CodeやPHPStormなどのIDEもPHPDocを使う。 | ||
=== Definition of a | === Definition of a Type === | ||
[https://docs.phpdoc.org/guide/references/phpdoc/types.html#definition-of-a-type phpDocumentor] | [https://docs.phpdoc.org/guide/references/phpdoc/types.html#definition-of-a-type phpDocumentor] | ||
| 21行目: | 21行目: | ||
型でnullを許容する場合、PHPDocではint|nullのように|で型をunion型のように書く。 | 型でnullを許容する場合、PHPDocではint|nullのように|で型をunion型のように書く。 | ||
PHP 7.1以上だと?intの書き方が許容されているが、PHPDocは| | PHP 7.1以上だと?intの書き方が許容されているが、PHPDocは|だけ。null|のように先頭に持ってくるのがわかりやすい。型は長いことがあるから。 | ||
==phpDocumentor== | === Tag === | ||
==== General ==== | |||
よく使う@param/@returnの構文。 | |||
<type-expression = 1*(array-of-type-expression|array-of-type|type ["|"]) | |||
array-of-type-expression = "(" type-expression ")[]" | |||
array-of-type = type "[]" | |||
type = class-name|keyword | |||
class-name = 1*CHAR | |||
keyword = "string"|"integer"|"int"|"boolean"|"bool"|"float" | |||
|"double"|"object"|"mixed"|"array"|"resource"|"scalar" | |||
|"void"|"null"|"callable"|"false"|"true"|"self" | |||
クラス名以外は全小文字。 | |||
基本は @<directive> <Type> <name> <description> の書式。スペース区切り。 | |||
*[https://docs.phpdoc.org/guide/references/phpdoc/tags/property.html @property]: __get/__setのマジックプロパティーを使う場合にクラスの注釈部で指定する。基本は使わない。が、型定義のない親クラスの型の明示にも使える。 | |||
*[https://docs.phpdoc.org/guide/references/phpdoc/tags/var.html @var]: 変数、プロパティー、定数で使用する。一番よく使う。 | |||
/** @var int $int This is a counter. */ | |||
$int = 0; | |||
// There should be no docblock here. | |||
$int++; | |||
class Foo | |||
{ | |||
/** | |||
* Full docblock with a summary. | |||
* | |||
* @var int | |||
*/ | |||
const INDENT = 4; | |||
/** @var string|null Short docblock, should contain a description. */ | |||
protected $description = null; | |||
public function setDescription($description) | |||
{ | |||
// There should be no docblock here. | |||
$this->description = $description; | |||
} | |||
} | |||
==== inline tag reference ==== | |||
[https://docs.phpdoc.org/guide/references/phpdoc/inline-tags/index.html#inline-tag-reference inline tag reference - phpDocumentor] | |||
以下のタグはインラインでも使用可能。ツールによって若干解釈方法が異なる。 | |||
* @example | |||
* @internal | |||
* @inheritdoc | |||
* @link | |||
* @see | |||
以下の書式で全体を波括弧で囲んで使う。 | |||
{@tag value} | |||
==== inheritance ==== | |||
[https://docs.phpdoc.org/guide/references/phpdoc/basic-syntax.html phpDocumentor] | |||
以下の継承関係、グループがある。 | |||
{| class="wikitable" | |||
!Elements | |||
!Inherited tags | |||
|- | |||
|''Any'' | |||
|[[/docs.phpdoc.org/guide/references/phpdoc/tags/author.html|@author]], [[/docs.phpdoc.org/guide/references/phpdoc/tags/version.html|@version]], [[/docs.phpdoc.org/guide/references/phpdoc/tags/copyright.html|@copyright]] | |||
|- | |||
|''Classes and Interfaces'' | |||
|[[/docs.phpdoc.org/guide/references/phpdoc/tags/category.html|@category]], [[/docs.phpdoc.org/guide/references/phpdoc/tags/package.html|@package]], [[/docs.phpdoc.org/guide/references/phpdoc/tags/subpackage.html|@subpackage]] | |||
|- | |||
|''Methods'' | |||
|[[/docs.phpdoc.org/guide/references/phpdoc/tags/param.html|@param]], [[/docs.phpdoc.org/guide/references/phpdoc/tags/return.html|@return]], [[/docs.phpdoc.org/guide/references/phpdoc/tags/throws.html|@throws]] | |||
|- | |||
|''Properties'' | |||
|[[/docs.phpdoc.org/guide/references/phpdoc/tags/var.html|@var]] | |||
|} | |||
==== Methods ==== | |||
===== @return ===== | |||
[https://docs.phpdoc.org/guide/references/phpdoc/tags/return.html#return phpDocumentor] | |||
https://chatgpt.com/c/67998298-f248-800b-a921-a47210ab8d72 | |||
関数やメソッドの説明のために関数定義の前に書くのが正しい使い方。 | |||
return文の直前に書くものではない。returnのところを説明したかったら、通常コメント。 | |||
ただ、関数の@returnでreturnが何を返すのかを書いた方がいい。 | |||
===== @throws ===== | |||
例外が発生する場合に記載する。複数の種類の例外がありえるなら、その数だけ@throwsを記載する。 | |||
==== 注意喚起 ==== | |||
https://chatgpt.com/c/67998298-f248-800b-a921-a47210ab8d72 | |||
注意が必要な挙動を文書化したい場合。@warnや@noticeはない。代わりの気泡を使う。 | |||
{| class="wikitable" | |||
!タグ | |||
!使い道 | |||
|- | |||
|<code>@todo</code> | |||
|'''修正や機能追加が必要な場合''' | |||
|- | |||
|<code>@deprecated</code> | |||
|'''非推奨の機能を警告''' | |||
|- | |||
|<code>@throws</code> | |||
|'''例外が発生する可能性を示す''' | |||
|- | |||
|<code>//</code> <code>/* */</code> | |||
|'''特定の処理に対する注意を記述''' | |||
|} | |||
迷ったら/** */でいいと思われる。 | |||
==== マジックメソッド ==== | |||
https://chatgpt.com/c/67a5c298-bdf8-800b-9f42-8618f36274ab | |||
独自クラスで__getや__setで独自のマジックメソッドで動的プロパティーを実装している場合。PHPDocがないと型や補完が一切されなくて辛い。 | |||
/** | |||
* クラスの説明 | |||
* | |||
* @property-read string $name ユーザーの名前 | |||
* @property int $age ユーザーの年齢 | |||
*/ | |||
class User { | |||
private array $data = []; | |||
public function __get(string $name) { | |||
return $this->data[$name] ?? null; | |||
} | |||
public function __set(string $name, $value): void { | |||
$this->data[$name] = $value; | |||
} | |||
} | |||
$name/$ageのプロパティー名のプロパティーが動的に追加されるなら、@propertyで変数名と型を書いておくと補完してくれる。 | |||
https://chatgpt.com/c/67a9546f-904c-800b-9459-af827e0b9fee | |||
__getに@returnしてもいいが、これはプロパティーの情報がない。__getに@returnするくらいなら、@property(-read) でプロパティーとセットで書いた方がいい。 | |||
=== Other === | |||
==== Array ==== | |||
* [https://docs.phpdoc.org/guide/references/phpdoc/types.html#arrays Definition of a 'Type' - phpDocumentor] | |||
* [https://docs.phpdoc.org/guide/guides/types.html#arrays Supported Types - phpDocumentor] | |||
PHPでよく使うArray。PHPDocでの表現方法がある。 | |||
# @return array | |||
# @return int[] | |||
# @return int[][] | |||
# @return (int|string)[] | |||
PHPDocでの配列の表現方法は以上。単純配列の配列の配列の場合、[]を増やせばいい。 | |||
連想配列については説明なし。arrayしかない。単純配列はPHPDocのこれで問題ない。問題は連想配列。 | |||
==== ArrayShape ==== | |||
* [https://psalm.dev/docs/annotating_code/type_syntax/array_types/ Array types - Documentation] | |||
* [https://phpstan.org/writing-php-code/phpdoc-types PHPDoc Types | PHPStan] | |||
* [https://blog.jetbrains.com/phpstorm/tag/arrayshape/ ArrayShape : The PhpStorm Blog | The JetBrains Blog] | |||
* [https://pleiades.io/phpstorm/whatsnew/2022-1/ PhpStorm の新機能 - 究極の進化を遂げた最強の PHP 開発環境] | |||
PHPDocでは未対応の連想配列だが、他の静的解析ツールの独自拡張で対応している。ArrayShapeという注釈。generics arrayとも呼んでいる。 | |||
いくつかの記法がある。 | |||
function getUserData(): array { | |||
return [ | |||
'name' => 'John Doe', | |||
'age' => 30, | |||
'email' => 'john.doe@example.com', | |||
]; | |||
} | |||
* @return array{name: string, age: int, email: string} | |||
/** | |||
* @return array{ | |||
* user: array{name: string, age: int, email: string}, | |||
* address: array{city: string, zip: int} | |||
* } | |||
*/ | |||
@var array{foo: int, bar?: string} <var-name> 説明。 | |||
@var array<array{foo: int, bar?: string}> <var-name> 説明。 | |||
[https://tech.designone.jp/entry/2022/08/18/152447 【PHP】タイプヒンティングをより強力にするArrayShape - デザインワン・ジャパン Tech Blog] | |||
こういう書式。ただし、この書式だと要素は説明できないので変数全体のところに説明を入れる。 | |||
連想配列のキーが可変の場合。以下のように<>で型だけ指定する。{}はキー名が決まっている場合。 | |||
array<string, int> | |||
リスト型で、要素ごとに型が決まっているなら、キー部分を数字にすると型を明示できる。 | |||
* @return array{0: string, 1: int, 2: string} | |||
/** | |||
* @param array{ | |||
* 0: string, // ユーザー名 | |||
* 1: int, // 年齢 | |||
* 2: bool // アクティブフラグ | |||
* } $userData | |||
*/ | |||
function processUser(array $userData): void { | |||
// ... | |||
} | |||
/** | |||
* @param list{ | |||
* string, // ユーザー名 | |||
* int, // 年齢 | |||
* bool // アクティブフラグ | |||
* } $userData | |||
*/ | |||
function processUser(array $userData): void { | |||
// ... | |||
} | |||
/** @var array{0: string, 1: int, 2: bool} $userData ユーザー情報:名前, 年齢, アクティブ */ | |||
3番目の形式がいい気がする。 | |||
==== stdClass ==== | |||
https://chatgpt.com/c/67b833b8-1d20-800b-bc27-16dfa133e3b0 | |||
関数で複数の値を一度に返したい時がある。その際の選択肢はオブジェクトか配列。 | |||
オブジェクトはstdClass。stdClassは動的にプロパティーを設定する前提。だが、phpdocでうまく認識してくれない。 | |||
@var stdClassでこの後ろに説明を掛けるくらい。 | |||
配列の方がまだいいか。 | |||
==== プロパティーの継承 ==== | |||
https://chatgpt.com/c/67cab3d0-eac4-800b-96c3-85ea480d1d91 | |||
子クラス側で再定義してしまうと、そちらが優先される。再定義しなければ、親クラスのプロパティーのphpdocが使われる。 | |||
[https://tech-blog.rakus.co.jp/entry/20210326/php 型を使いこなすためのPHPDocの書き方 - RAKUS Developers Blog | ラクス エンジニアブログ] | |||
例えば、外部のフレームワークを使っていて、そのベースクラス側で型定義がない場合、継承後に@propertyで使いたいプロパティーの型を明示できる。 | |||
この方法を使わない場合、同じプロパティーを初期値指定で再定義しないといけない。@propertyで指定するとそれを回避できる。 | |||
=== phpDocumentor === | |||
Ref: [https://phpdoc.org/ Home | phpDocumentor]. | Ref: [https://phpdoc.org/ Home | phpDocumentor]. | ||
| 151行目: | 396行目: | ||
他に@see/@tutorialも使える。が、専用のものはない。/** */ 内でUsageなどで普通の文章で説明するしかない。 | 他に@see/@tutorialも使える。が、専用のものはない。/** */ 内でUsageなどで普通の文章で説明するしかない。 | ||
==Package manager== | ==Package manager== | ||
===Composer=== | ===Composer=== | ||
[https://getcomposer.org/ Composer] | |||
PHPのパッケージ管理システム。PHP v5.3.2以上で動作する。 | PHPのパッケージ管理システム。PHP v5.3.2以上で動作する。 | ||
====Install==== | ====Install==== | ||
Ref: [https://senooken.jp/post/2020/01/16/ インストール: Composer | モダンなPHPのパッケージマネージャー – senooken JP]. | Ref: [https://senooken.jp/post/2020/01/16/ インストール: Composer | モダンなPHPのパッケージマネージャー – senooken JP]. | ||
LOCAL=~/.local PKG=composer VER= DIR=$LOCAL/stow/$PKG-$VER/bin | |||
[ -e installer ] || wget <nowiki>https://getcomposer.org/installer</nowiki> | [ -e installer ] || wget <nowiki>https://getcomposer.org/installer</nowiki> | ||
php installer --install-dir="$ | [ -e installer ] || curl -LO <nowiki>https://getcomposer.org/installer</nowiki> | ||
mkdir -p $DIR | |||
php installer --install-dir="$DIR" --filename=$PKG | |||
公式サイトからinstallerをダウンロードしてそれを使って任意の場所に設置する。 | 公式サイトからinstallerをダウンロードしてそれを使って任意の場所に設置する。 | ||
[https://getcomposer.org/download/ Composer] | |||
php -r "copy('<nowiki>https://getcomposer.org/installer'</nowiki>, 'composer-setup.php');" | |||
php composer-setup.php --install-dir=$HOME/.local/bin --filename=composer | |||
これでもいい。 | |||
====Usage==== | ====Usage==== | ||
[https://getcomposer.org/doc/01-basic-usage.md Basic usage - Composer] | |||
Composerを使う場合,<code>composer.json</code>ファイルを用意する。このファイルはプロジェクトの依存関係を記載する。VCSで管理すべきファイルだ。 | Composerを使う場合,<code>composer.json</code>ファイルを用意する。このファイルはプロジェクトの依存関係を記載する。VCSで管理すべきファイルだ。 | ||
| 309行目: | 451行目: | ||
<code>composer update monolog/monolog</code> | <code>composer update monolog/monolog</code> | ||
Composerでインストール可能な主なパッケージは,Packagistリポジトリーに配置されている。 | Composerでインストール可能な主なパッケージは,Packagistリポジトリーに配置されている。 | ||
====自動読み込み | ====Autoloading (自動読み込み )==== | ||
===== About ===== | |||
[https://getcomposer.org/doc/01-basic-usage.md#autoloading Basic usage - Composer] | |||
ライブラリーの自動読み込みのために,Composerは<code>vendor/autoload.php</code>ファイルを生成する。以下のように,ライブラリーのクラスを使用するファイルの先頭でこのファイルを読み込めば使えるようになる。 | ライブラリーの自動読み込みのために,Composerは<code>vendor/autoload.php</code>ファイルを生成する。以下のように,ライブラリーのクラスを使用するファイルの先頭でこのファイルを読み込めば使えるようになる。 | ||
<require __DIR__ . '/vendor/autoload.php'; | <php | ||
require __DIR__ . '/vendor/autoload.php'; | |||
$log = new Monolog\Logger('name'); | $log = new Monolog\Logger('name'); | ||
$log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING)); | $log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING)); | ||
$log->addWarning('Foo'); | $log->addWarning('Foo'); | ||
このほかに、composer.jsonの<code>autoload</code>欄に指定することで,ライブラリー以外の自分のコードも自動読み込みの対象にできる。 | |||
{ | |||
"autoload": { | |||
"psr-4": {"Acme\\": "src/"} | |||
} | |||
} | |||
===== PSR-4 ===== | |||
[https://www.php-fig.org/psr/psr-4/ PSR-4: Autoloader - PHP-FIG] | |||
psr-4はPHPの標準仕様。 | |||
名前空間、クラスメイト、ファイルパスとの対応関係を記している。 | |||
# 名前空間の単位ごとに、ディレクトリーに対応。 | |||
# 大文字小文字は区別。 | |||
# 大文字小文字を区別して、ファイル名は.phpの拡張子で終わる。 | |||
===== dump-autoload ===== | |||
composer.jsonを編集した場合、<code>composer dump-autoload</code>を実行して<code>vendor/autolaod.php</code>を必ず更新します。 | composer.jsonを編集した場合、<code>composer dump-autoload</code>を実行して<code>vendor/autolaod.php</code>を必ず更新します。 | ||
https://chatgpt.com/share/6837bdb1-1798-800b-a982-650ce38bc30a | |||
なお、require_onceではなくrequireになっているのには理由がある。 | |||
* そもそも何回も読み込む用途ではない。普通アプリのルートで1回しか読み込まない。 | |||
* 万が一複数回読み込んでも、内部的にspl_autoload_registerを呼んでいて、重複登録されない。 | |||
* requireのほうが読み込み確認がないぶんわずかに早い。 | |||
==== Libraries ==== | ==== Libraries ==== | ||
| 391行目: | 560行目: | ||
composer.jsonにscriptsを含めることできるし、package.jsonでもOK。 | composer.jsonにscriptsを含めることできるし、package.jsonでもOK。 | ||
==== Repositories ==== | |||
[https://getcomposer.org/doc/05-repositories.md Repositories - Composer] | |||
パッケージとリポジトリーの概念。 | |||
===== Concepts ===== | |||
====== Package ====== | |||
パッケージは何かを含むディレクトリー。名前とバージョンを含んでいて、パッケージを識別する。 | |||
====== Repository ====== | |||
パッケージのソース。パッケージとバージョンのリスト。 | |||
===== Types ===== | |||
リポジトリーの種類を指定する。 | |||
* composer: デフォルト | |||
* vcs: gitリポジトリー。ローカルのgitリポジトリーもurlにパス指定で対応できるが、基本はURL。ローカルはtype=path指定で対応する。 | |||
* package: zipファイルなど。 | |||
===== Hosting your own ===== | |||
外部のホスティングサービス、サイトなどに配置していないものをリポジトリーとして使いたいことがあったりする。 | |||
例えば、社内のライブラリーのビルド結果とか。プライベートなリポジトリーとか。 | |||
* type=artifact: ZIPやtarファイル。ルートにcomposer.jsonを含む。 | |||
* type: path: 絶対パス、相対パスでローカルディレクトリーを指定。VCSの場合、type=vcsでurlを相対パスでもできるが、ローカルの場合はpathにするのがよさそう。。バージョンは現在のブランチ・タグから推測する。あるいは、パッケージのcomposer.jsonで明示。どれでも解決できない場合、dev-masterとみなす。 | |||
==== Articles ==== | ==== Articles ==== | ||
| 508行目: | 705行目: | ||
PHPのバージョンアップの互換性の確認などもできる。 | PHPのバージョンアップの互換性の確認などもできる。 | ||
=== php -l === | ==== 古いスタイルのPHPの改善 ==== | ||
https://grok.com/share/c2hhcmQtMw%3D%3D_938039fe-7a79-4b4b-aad5-6b0fb3e71c8f | |||
https://grok.com/share/c2hhcmQtMw%3D%3D_87563ab0-56ca-4faa-aa15-f5fb31dcf52d | |||
昔ながらの、要請が来たらheader/echoで手動で応答を返すスタイルのPHPコード。このままだと、テストとかしにくいし、同一コードが散見して冗長になる。保守に問題がある。 | |||
改善手順がある。 | |||
# 静的解析ツール (php -l/phpstan) | |||
# 仕様化テスト (phpunit) | |||
# リファクタリング (関数抽出) | |||
# リファクタリング (クラス化) | |||
# 単体テスト | |||
===== 仕様化テスト/Characterization test ===== | |||
[https://maku.blog/p/p6awy3z/ 読書メモ『レガシーコード改善ガイド』マイケル・C・フェザーズ|まくろぐ] | |||
特徴付けテストと直訳することもあるらしい。マイケル・C・フェザーズの2009年翻訳のレガシーコード改善ガイドで提唱された内容。著者の独自の概念。 | |||
ドキュメントや仕様が不明瞭なコードを扱う際に有効。既存コードの動作を把握し、そのふるまいを固定化するための手法。 | |||
現状コードが今何をするのかをテストで記録する。リファクタリングとかをする前に現在の動作をある程度保証するテストを記載することで、意図しない影響を防げる。 | |||
例えば、昔ながらの、phpファイルがそのまま応答を返すタイプのアプリの場合。 | |||
環境変数を設定して、バッファリングで、文字列の有無で、ある程度現在のふるまいをテストできる。 | |||
PHPUnitでやる場合。 | |||
<?php declare(strict_types=1); | |||
namespace Tests\Unit\Service\Resign; | |||
final class SsResignConfirmTest extends \Tests\Unit\BaseTestCase | |||
{ | |||
public function testBaseline(): void | |||
{ | |||
$this->assertBaseline('service/resign/ss_resign_confirm.php'); | |||
} | |||
} | |||
<?php declare(strict_types=1); | |||
namespace Tests\Unit; | |||
/** | |||
* 単体テストのベースクラス。 | |||
* | |||
* tests/Unit/以下はこのクラスを継承して実装する。基本は、継承先の子クラスのtestBaselineでassertBaselineを呼び出す。 | |||
* | |||
* 他に、テストで共通で行うようなものがあれば、こちらに記述して共通化していく。 | |||
*/ | |||
abstract class BaseTestCase extends \PHPUnit\Framework\TestCase | |||
{ | |||
/** | |||
* 仕様化テスト (characterization test) として、該当ファイルの応答にエラーがないことを確認する共通テストメソッド。 | |||
* | |||
* 継承先の子クラスのtestBaseline内で `$this->assertBaseline('path');` で実行想定。 | |||
* @param string $relative_include_path テスト対象のファイルの相対パス。 | |||
*/ | |||
protected function assertBaseline(string $relative_include_path): void | |||
{ | |||
ob_start(); | |||
include PROJECT_ROOT . '/' . $relative_include_path; | |||
$html = ob_get_clean(); | |||
$this->assertStringNotContainsString('URLが不正です。', $html); | |||
/** err.tpl */ | |||
$this->assertStringNotContainsString('<title>エラー', $html); | |||
} | |||
こんな感じで、output bufferingでechoのHTTP本体相当を変数に格納して、その変数にエラー文字がないかでチェックする。 | |||
https://grok.com/share/c2hhcmQtMw%3D%3D_d3073260-4256-46a8-8762-ef7c63967681 | |||
仕様化テストだから、Baseじゃなくて、Baselineとするのがいい。assertBaseline/testBaselineでやると、一貫性ある命名規則にできる。 | |||
https://grok.com/share/c2hhcmQtMw%3D%3D_57517641-d440-468e-ae2e-956958ce27f9 | |||
API系の場合、結合テストに近い。サーバーを立てて、リクエストを送って、レスポンスがどうなるかをテストする感じ。 | |||
一部クラスになっている場合も、基本は現在の挙動確認。現在の挙動を確認するような動作のテストをする。最終出力回りを重点的に書くとよい。 | |||
===== 関数抽出 ===== | |||
<?php | |||
require 'smarty_setup.php'; // Smarty初期化 | |||
header('Content-Type: text/html; charset=UTF-8'); | |||
$id = $_GET['id']; | |||
$db = new PDO('...'); // DB接続 | |||
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?"); | |||
$stmt->execute([$id]); | |||
$user = $stmt->fetch(); | |||
$smarty->assign('user', $user); | |||
echo $smarty->fetch('user.tpl'); | |||
?> | |||
<?php | |||
function fetchUser($db, $id) { // ロジック関数 | |||
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?"); | |||
$stmt->execute([$id]); | |||
return $stmt->fetch(); | |||
} | |||
function renderUser(Smarty $smarty, $user) { // ビュー関数 | |||
$smarty->assign('user', $user); | |||
return $smarty->fetch('user.tpl'); // echoせず返す | |||
} | |||
// メインスクリプト | |||
require 'smarty_setup.php'; | |||
header('Content-Type: text/html; charset=UTF-8'); | |||
$db = new PDO('...'); | |||
$id = $_GET['id']; | |||
$user = fetchUser($db, $id); | |||
$output = renderUser($smarty, $user); | |||
echo $output; | |||
?> | |||
こんな感じで、内容ごとに処理を関数にまとめる。 | |||
# header出力 | |||
# DB操作 | |||
# テンプレート操作 | |||
関数化した部分のテストは、仕様化テストと同様で、phpunitでoutput bufferingで出力部分を破棄して、関数定義だけ取り込んでテストする。 | |||
長期保守前提であれば、関数化の段階をすっ飛ばして、オブジェクト・クラス化したほうが手っ取り早い。 | |||
===== クラス化 ===== | |||
https://grok.com/share/c2hhcmQtMw%3D%3D_21361425-16d6-419a-84ae-3c793d627eef | |||
シンプルで無難なMVCの形でクラス化する。 | |||
* DB操作: Modelクラス | |||
* 画面表示: Viewクラス | |||
* ロジック呼び出し: Controllerkクラス | |||
'''コード例''': | |||
Model (models/UserModel.php): | |||
class UserModel { | |||
private $db; | |||
public function __construct(PDO $db) { | |||
$this->db = $db; | |||
} | |||
public function fetchUser($id) { | |||
$stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?"); | |||
$stmt->execute([$id]); | |||
return $stmt->fetch(); | |||
} | |||
} | |||
View (views/UserView.php): | |||
class UserView { | |||
private $smarty; | |||
public function __construct(Smarty $smarty) { | |||
$this->smarty = $smarty; | |||
} | |||
public function renderUser($user) { | |||
$this->smarty->assign('user', $user); | |||
return $this->smarty->fetch('user.tpl'); | |||
} | |||
} | |||
Controller (controllers/UserController.php): | |||
class UserController { | |||
private $model; | |||
private $view; | |||
public function __construct(UserModel $model, UserView $view) { | |||
$this->model = $model; | |||
$this->view = $view; | |||
} | |||
public function handleRequest() { | |||
$id = $_GET['id']; | |||
$user = $this->model->fetchUser($id); | |||
return $this->view->renderUser($user); | |||
} | |||
} | |||
メインスクリプト (user.php): | |||
<?php | |||
require 'autoload.php'; // Composerのautoload | |||
$db = new PDO('...'); | |||
$smarty = new Smarty(); // Smartyセットアップ | |||
$model = new UserModel($db); | |||
$view = new UserView($smarty); | |||
$controller = new UserController($model, $view); | |||
header('Content-Type: text/html; charset=UTF-8'); | |||
$output = $controller->handleRequest(); | |||
echo $output; | |||
?> | |||
これでMVCパターンに近づく。SmartyをViewクラスでラップしてテストしやすく。 | |||
あるいは、メインスクリプトをControllerとみなして、constructorで画面表示処理を書くというのもありかもしれない。 | |||
if (!debug_backtrace()) {} | |||
このコードでincludeとの違いも検知できる。 | |||
PDOなどのDB接続インスタンスは、メインスクリプトで従来通り毎回書くか、シングルトンかstaticメソッドで管理して渡すとか。 | |||
===== .inc/.include/.class ===== | |||
https://grok.com/share/c2hhcmQtMw%3D%3D_6193ad5e-6df3-42c7-9dbc-dc67deb44bcd | |||
phpファイルが直接応答を返す古いパターンだと、拡張子が.inc/.include/.classなど.phpじゃないことがある。 | |||
公開ディレクトリーに、同居する都合、サーバーアクセスで拡張子で、アクセス制御しているからだろう。 | |||
本来であれば、現代的なpublic/index.phpでコントローラーを明示的に振り分けて、ユーザーがアクセス可能なディレクトリーを制限すべきだろう。 | |||
=== PHP === | |||
==== declare(strict_types=1); ==== | |||
新規ファイルには、基本的に指定したほうがいい。既存ファイルは慎重に。 | |||
==== php -l ==== | |||
* [https://ja.wikipedia.org/wiki/%E9%9D%99%E7%9A%84%E3%82%B3%E3%83%BC%E3%83%89%E8%A7%A3%E6%9E%90 静的コード解析 - Wikipedia] | * [https://ja.wikipedia.org/wiki/%E9%9D%99%E7%9A%84%E3%82%B3%E3%83%BC%E3%83%89%E8%A7%A3%E6%9E%90 静的コード解析 - Wikipedia] | ||
* [https://www.php.net/manual/ja/features.commandline.options.php PHP: オプション - Manual] | * [https://www.php.net/manual/ja/features.commandline.options.php PHP: オプション - Manual] | ||
| 523行目: | 931行目: | ||
-lは-r (コードの実行)とは併用不能。以下のコードで全phpファイルをチェックできる。 | -lは-r (コードの実行)とは併用不能。以下のコードで全phpファイルをチェックできる。 | ||
for i in `find . -name \*.php`; do php -l $i | grep -v "No syntax errors"; done | for i in `find . -name \*.php`; do php -l $i | grep -v "No syntax errors"; done | ||
上記コードより、以下の方が少し早い。 | |||
find . -name \*.php -exec php -l {} \; | grep -v '^No syntax errors' | |||
外部ツールも不要で手軽で悪くない。特に、-lで実行するphpのバージョンを変えるだけでチェックでき、PHPのバージョンアップ時の事前の簡易チェック、粗探しとしてとして悪くない ([https://tech-blog.rakus.co.jp/entry/20220922/php PHPerのための「静的解析」を語り合う【PHP TechCafe イベントレポート】 - RAKUS Developers Blog | ラクス エンジニアブログ])。 | 外部ツールも不要で手軽で悪くない。特に、-lで実行するphpのバージョンを変えるだけでチェックでき、PHPのバージョンアップ時の事前の簡易チェック、粗探しとしてとして悪くない ([https://tech-blog.rakus.co.jp/entry/20220922/php PHPerのための「静的解析」を語り合う【PHP TechCafe イベントレポート】 - RAKUS Developers Blog | ラクス エンジニアブログ])。 | ||
ただ、php -lは構文エラーのみで、型チェックはできない。PHPStanを使うしかない模様。 | |||
gitのpre-commitに登録する場合、以下のような内容にするとよい。 | |||
#!/bin/sh | |||
## Lint added/modified PHP file. | |||
set -eu | |||
has_error=false | |||
PHP_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.php$' || :) | |||
[ -z "$PHP_FILES" ] && exit || : | |||
while read -r file; do | |||
php -l "$file" | grep -v '^No syntax errors' && has_error=true | |||
done <<-EOT | |||
$PHP_FILES | |||
EOT | |||
$has_error && exit 1 || : | |||
./vendor/bin/phpstan analyze --memory-limit 5G $PHP_FILES || has_error=true | |||
$has_error && exit 1 || : | |||
# parameters.pathsのコメントアウトを解除して全体解析する場合、上記のファイル単位のphpstanをやめて、以下の全体解析を実行する。 | |||
# ./vendor/bin/phpstan analyze --memory-limit 5G || exit 1 | |||
ただ、根本的なPHPの構文エラーがあると、PHPStanはそこで失敗して詳細情報がない。php -lもあっていい気がする。 | |||
=== PHPStan === | === PHPStan === | ||
* [https://tech.hajimari.inc/entry/2022/06/16/120000 PHPStan導入のすすめ - Hajimari Tech Blog| 株式会社Hajimari] | * [https://tech.hajimari.inc/entry/2022/06/16/120000 PHPStan導入のすすめ - Hajimari Tech Blog| 株式会社Hajimari] | ||
* [https://zenn.dev/pixiv/articles/7467448592862e PHPStanクイックガイド2023] | * [https://zenn.dev/pixiv/articles/7467448592862e PHPStanクイックガイド2023] | ||
導入が簡単なので黙って導入したらよさそう。 | 導入が簡単なので黙って導入したらよさそう。 | ||
==== User Guide ==== | |||
===== Getting Started ===== | |||
[https://phpstan.org/user-guide/getting-started Getting Started | PHPStan] | |||
composer require --dev phpstan/phpstan | |||
以下のコマンドでバージョンを確認できればOK。 | |||
vendor/bin/phpstan analyze --version | |||
PHPStan - PHP Static Analysis Tool 2.1.10 | |||
====== git pre-commit ====== | |||
hooks/pre-commit | |||
#!/bin/sh | |||
## Lint added/modified PHP file. | |||
set -eu | |||
has_error=false | |||
PHP_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.php$' || :) | |||
[ -z "$PHP_FILES" ] && exit || : | |||
while read -r file; do | |||
php -l "$file" | grep -v '^No syntax errors' && has_error=true | |||
done <<-EOT | |||
$PHP_FILES | |||
EOT | |||
$has_error && exit 1 || : | |||
./vendor/bin/phpstan analyze --memory-limit 5G $PHP_FILES || has_error=true | |||
$has_error && exit 1 || : | |||
# parameters.pathsのコメントアウトを解除して全体解析する場合、上記のファイル単位のphpstanをやめて、以下の全体解析を実行する。 | |||
# ./vendor/bin/phpstan analyze --memory-limit 5G || exit 1 | |||
# phpstan.dist.neon | |||
includes: | |||
- phpstan-baseline.neon | |||
parameters: | |||
level: 0 | |||
tmpDir: var/tmp/phpstan | |||
# parallel: | |||
# processTimeout: 15000.0 | |||
# scanDirectories: | |||
# - ../../ | |||
# todo ある程度指摘対応ができたら常に全体解析にする。 | |||
# paths: | |||
excludePaths: | |||
- var | |||
最初は修正ファイルだけphpstanで解析して、安定してきたらプロジェクト全体を解析する形にするとよいだろう。 | |||
phpstanのファイル単独実行は時間がかかるので、修正対象をまとめて実行した方がいい。 | |||
https://grok.com/share/c2hhcmQtMw%3D%3D_85f34631-62c7-426a-89cb-ecb407c0246b | |||
なお、WSLやDockerを使っている場合、docker exec <container-name> php -lなどで、docker内のphp/phpstanをホスト側で使うのがいい。 | |||
docker runで起動している場合、--nameでコンテナー名を固定する。 | |||
===== Command Line Usage ===== | |||
[https://phpstan.org/user-guide/command-line-usage Command Line Usage | PHPStan] | |||
====== Analyzing code ====== | |||
<code>vendor/bin/phpstan analyse [options] [<paths>...]</code> | |||
いくつか重要なオプションがある。 | |||
* paths: 検査対象ファイルパス。設定ファイルで指定可能。 | |||
* --level|-l: 実行レベル。設定ファイルで指定可能。 | |||
* --configuration|-c: 設定ファイルを指定する。 | |||
* --generate-baseline|-b: ベースラインを作成する。オプション引数で出力ファイルのパスを指定できる。デフォルトはphpstan-baseline.neon。 | |||
* --memory-limit: php.iniと同じ形式で最大メモリーを指定。 | |||
====== Running without arguments ====== | |||
PHPStanは基本的に、コマンド引数で指定した、ディレクトリー類を対象に解析する。 | |||
毎回コマンド引数を指定するのは手間なので、設定ファイルに記述しておくこともできる。 | |||
以下の条件を満たせば、引数なしで、設定ファイルの内容で解析できる。 | |||
# phpstan.neonかphpstan.neon.distの存在。 | |||
# pathsに解析対象パスリストが存在。 | |||
# levelパラメーターの存在。 | |||
最小限の例。 | |||
parameters: | |||
level: 0 | |||
paths: | |||
- src | |||
- tests | |||
現実的な例。 | |||
includes: | |||
- phpstan-baseline.neon | |||
parameters: | |||
level: 0 | |||
parallel: | |||
processTimeout: 15000.0 | |||
tmpDir: var/tmp/phpstan | |||
scanDirectories: | |||
paths: | |||
excludePaths: | |||
====== Minimum file ====== | |||
https://chatgpt.com/c/67fe029c-9ce0-800b-9397-8e1d7f18b303 | |||
phpstanは基本的に引数で<paths>でディレクトリーやファイルを指定する。ただし、ここで指定していないファイルは、見に行けないので未定義の警告などが出る。 | |||
基本はパス全体を指定する。コマンドライン引数か、phpstan.dist.neonで指定しておく。 | |||
他に、pre-commitように、autoload_filesかbootstarpFilesでベースの依存ファイルを指定する。 | |||
他に、phpstanはcomposerのautoloadを理解するので、composer.jsonにautoloadを追加する。 | |||
====== Reflection error: Circular reference to class ====== | |||
名前通り循環参照が起きている。具体的に、自分と同名のクラスを最終的にextendsしている。 | |||
元ファイルを直す他、excludePathsで検査対象などから除外すれば回避できる。 | |||
====== Internal error: Child process timed out after 3000.0 seconds. Try making it longer with parallel.processTimeout setting. ====== | |||
https://chatgpt.com/share/68020423-6350-800b-9200-3d8c2d2483e2 | |||
--generate-baselineを指定すると、初回だけ時間がかかる。 | |||
parallel.processTimeoutでタイムアウトの時間を設定する。成功させるために、進捗率と現在の設定の比率でカバーできるだけの時間にする。3000で40 %くらいの進捗だったら9000とか。 | |||
初回だけ時間がかかる。 | |||
====== Syntax error, unexpected T_EXTENDS on line 18 ====== | |||
https://chatgpt.com/c/6801fda7-2434-800b-84da-601c44645488 | |||
phpstanを実行すると上記のような、エラーが出ることがある。これは、PHPStanの前に根本的なPHPの構文エラーの可能性が高い。 | |||
PHPStan実行の前に、先にphp -lで解決しておく必要がある。 | |||
time -p (find . -name \*.php -exec php -l {} \; | grep -v 'No syntax errors' >php-l.log) 2>&1 | |||
====== Error while loading phpstan-baseline.neon: Invalid UTF-8 sequence. ====== | |||
作成したBaselineをincludeで読み込んで実行すると上記のエラーが出た。 | |||
https://chatgpt.com/share/68020423-6350-800b-9200-3d8c2d2483e2 | |||
ベースラインファイルにへんな文字が入ったのが原因。 | |||
以下のコマンドで変な文字の行を特定できる。 | |||
grep -axv '.*' phpstan-baseline.neon | |||
--error-format=rawを指定してbaselineを作り直すと良い。 | |||
===== The Baseline ===== | |||
[https://phpstan.org/user-guide/baseline The Baseline | PHPStan] | |||
PHPStanはレベルベースでチェックしてくれる。が、既存コードで警告が多い場合、新規追加分だけ考慮したいだろう。 | |||
そういう場合に、ベースラインが役立つ。 | |||
以下のように、--generate-baselineオプションを指定して、PHPStanを実行すると、現在のエラーリストをエクスポートしてくれる。 | |||
vendor/bin/phpstan analyse --generate-baseline | |||
ベースラインファイルを使う場合、オプションで指定するか、設定ファイルで明記する。 | |||
デフォルトでphpstan-baseline.neon。オプション引数で、出力ベースラインファイルを指定できる。 | |||
作成したら、設定ファイルphpstan.dist.neonに読み込む。 | |||
includes: | |||
- phpstan-baseline.neon | |||
parameters: | |||
# your usual configuration options | |||
これで次回からはベースラインのエラーを無視してくれる。必要に応じて、ベースラインを編集したらい、ベースラインを再生成してもいい。 | |||
なお、ベースラインで無視したエラーがなくなった場合、ベースラインファイルから削除するまで、PHPStanは通知する。この通知を無効にしたかったら、以下を設定する。 | |||
parameters: | |||
reportUnmatchedIgnoredErrors: false | |||
なお、ベースライン作成時に、以下の警告が出ることがあり、全ての指摘をベースラインに出力できるわけではない。 | |||
[WARNING] Baseline generated with 79012 errors. | |||
Some errors could not be put into baseline. Re-run PHPStan with "-vv" | |||
and fix them. | |||
致命的なエラーはベースラインに出力できないので、直すしかない。 | |||
===== Discovering Symbols ===== | |||
[https://phpstan.org/user-guide/discovering-symbols Discovering Symbols | PHPStan] | |||
https://chatgpt.com/c/67e0b340-da84-800b-89c8-4c72691fdc44 | |||
pathsで指定したファイルで使用されているシンボルをPHPStanは必要とする。デフォルトで以下の2箇所を探す。 | |||
* pathsの対象 (コマンドライン引数と設定ファイル)。 | |||
* composerの依存関係 | |||
基本はこれで見つかるはず。 | |||
====== Third party code outside of Composer dependencies ====== | |||
PEARとか追加で設定が必要だったりする。そういう場合、scanFilesとscanDirectoriesの設定が使える。 | |||
これらで指定したファイル、ディレクトリーをシンボルとして探す。探すだけで解析はしない。 | |||
https://chatgpt.com/share/680b240a-bf28-800b-bfc2-a84f639fe0c0 | |||
プロジェクト外部はみれないらしい。 | |||
autoload_filesでautoload.phpを指定するか。bootstrapFilesで読み込む。 | |||
https://grok.com/share/c2hhcmQtMw%3D%3D_fc03d339-2ee1-4f76-85bd-ee567a2575ef | |||
例えば、PEARの依存関係を追加したい場合、以下のコマンドでpearのパスを確認する。 | |||
pear config-get php_dir | |||
これをscanDirectoriesに指定すればOK。 | |||
parameters: | |||
scanFiles: | |||
- /usr/local/php | |||
====== Bootstrap ====== | |||
https://grok.com/share/c2hhcmQtMw%3D%3D_fc03d339-2ee1-4f76-85bd-ee567a2575ef | |||
PHPStanの実行前に、PHPのランタイム設定したい場合、bootstrapFilesで自前の起動時ファイルを指定できる。 | |||
parameters: | |||
bootstrapFiles: | |||
- phpstan-bootstrap.php | |||
例えば、環境変数の設定とかをこれでうまくできる。独自のautoload的なことやっている場合もこれでいける。 | |||
===== Output Format ===== | |||
[https://phpstan.org/user-guide/output-format Output Format | PHPStan] | |||
--error-formatのオプションでPHPStanの報告形式を指定できる。 | |||
.neonファイルに以下の形式で指定可能。 | |||
parameters: | |||
errorFormat: json | |||
特に重要なものがある。 | |||
* table: デフォルト。 | |||
* raw: 1行1データで機械処理用。文字化けなどが起こりにくい。 | |||
* json: IDEなどとの連携用。 | |||
* checkstyle: XML形式。CIツール連携用。 | |||
困ったらrawを指定しておく。 | |||
===== Ignoring Errors ===== | |||
[https://phpstan.org/user-guide/ignoring-errors Ignoring Errors | PHPStan] | |||
====== Excluding whole files ====== | |||
====== Broken symbolic link ====== | |||
シンボリックリンクのリンク切れなどで以下のエラーが出ることがある。 | |||
Could not read file: | |||
https://chatgpt.com/c/68088168-ac54-800b-b78e-e28743b81011 | |||
excludePathsはファイルの中身に有効で、scanDirectiriesで指定したディレクトリーに、破損したシンボリックリンクがあると、スキャン対象に入った時点で失敗する。 | |||
事前に破損したシンボリックリンクを削除しておく必要がある。 | |||
以下のコマンドで破損リンクを削除できる。 | |||
find path/to/scan -type l ! -exec test -e {} \; -exec rm {} \; | |||
# find path/to/scan -type l ! -exec test -e {} \; -print # 確認用 | |||
===== Result Cache ===== | |||
PHPStanは分析実行結果をキャッシュする。条件は、解析対象ファイルリスト。同一じゃないとキャッシュは使えない。 | |||
キャッシュが使えると、数秒で解析が終わることもある。 | |||
====== tmpDir ====== | |||
実行結果は%tmpDir%/resultCache.phpになる。 | |||
sys_get_temp_dir() . '/phpstan' (usually /tmp/phpstan) | |||
parameters.tmpDirで上書き指定可能。 | |||
Macだと以下のような場所。 | |||
php -r 'echo sys_get_temp_dir() ."\n";' | |||
/var/folders/hr/9q7pmn9x5_788w3fsp9dd71r316wyt/T | |||
このtmpDirのデフォルトは、基本的にOSを再起動すると消去される。なので、基本的にneonファイルに以下のような指定がほぼ必須。 | |||
parameters:tmpDir: var/tmp/phpstan | |||
.gitignoreにもついでに以下を追加する。 | |||
/var/ | |||
====== scannedFiles ====== | |||
phpstanを実行すると以下のようにキャッシュの不一致が示されることがある。 | |||
time -p vendor/bin/phpstan analyze --memory-limit=5G --generate-baseline --error-format=raw -vv 2>&1 | tee phpstan.log | |||
Note: Using configuration file phpstan.dist.neon. | |||
Result cache not used because the metadata do not match: projectConfig, scannedFiles | |||
設定ファイルと、スキャンファイル一覧のどちらか。 | |||
原因把握のために、scannedFilesを確認したい。 | |||
resultCache.phpのscannedFilesの連想配列のキーに、ファイル一覧が入っている。 | |||
確認すると、.phpの拡張子のファイルが入っている。.gitとかは含んでいない。が、tmpDirに指定したキャッシュ内の.phpは見ている。除外必要。 | |||
parameters.fileExtensionsで追加可能 ([https://phpstan.org/config-reference#discovering-symbols Config Reference | PHPStan])。 | |||
====== paths/excludePath/scanDirectories ====== | |||
結果キャッシュに影響のある中で、scannedFilesに影響のあるこれらの設定の関係がわかりにくいので整理する。 | |||
* pathsの指定があれば含む。 | |||
* scanDirectoriesがあれば含む | |||
* excludePath (.analyseAndScan) の指定があれば、除外される。paths内で一部を除外する場合に使う。シンボルとしては見たい場合はexcludePath.analyseの指定。analyseと併用・別々に記載したい場合はanalayseAndScan。scanDirectories以下も同様。気持ち悪いが、analyzeではなくてanalyseじゃないと認識されない。 | |||
parameters: | |||
paths: | |||
- src | |||
excludePaths: | |||
analyse: | |||
- src/thirdparty | |||
analyseAndScan: | |||
- src/broken | |||
srcの中のsrc/thirdpartyを、解析対象に含めたくないが、シンボルとしては認識したい場合。src/brokenは見に行くとエラーになるのでシンブルとしても無視。 | |||
==== Config Reference ==== | |||
[https://phpstan.org/config-reference Config Reference | PHPStan] | |||
基本はphpstan.neon.distまたはphpstan.dist.neonをバージョン管理する。ユーザーはphpstan.neonで上書きできるようにする。 | |||
phpstan.dist.neonのほうが、拡張子が明示されてよく感じる。 | |||
===PHPUnit=== | ===PHPUnit=== | ||
PHPUnitを使用すれば自動単体テスト (Unit test) が可能だ。 | PHPUnitを使用すれば自動単体テスト (Unit test) が可能だ。 | ||
====Version==== | ==== About ==== | ||
===== Version ===== | |||
情報源: [https://phpunit.de/supported-versions.html Supported Versions of PHPUnit – The PHP Testing Framework]。 | 情報源: [https://phpunit.de/supported-versions.html Supported Versions of PHPUnit – The PHP Testing Framework]。 | ||
| 542行目: | 1,284行目: | ||
PHP v7.4に対応してい最後のバージョンはPHPUnit 9なので、当分はこれを使うのが良い。 | PHP v7.4に対応してい最後のバージョンはPHPUnit 9なので、当分はこれを使うのが良い。 | ||
===== Install ===== | |||
*[https://web.gnusocial.jp/post/2023/07/07/7428/ 設置: PHPUnit | PHPの定番テストフレームワーク | GNU social JP Web] | |||
以下のコマンドでPHP 7.4に対応している最後のPHPUnitのv9をインストール。 | |||
composer require --dev phpunit/phpunit ^9 | |||
composer.jsonに以下が追加される。 | |||
{ | |||
"require-dev": { | |||
"phpunit/phpunit": "^9" | |||
} | |||
} | |||
composer.jsonとcomposer.lockをVCSで管理する。 | |||
composerのautoloadのclassmapでソースファイルのルートディレクトリーを指定する。 | |||
{ | |||
"autoload": { | |||
"classmap": [ | |||
"src/" | |||
] | |||
}, | |||
"require-dev": { | |||
"phpunit/phpunit": "^9" | |||
} | |||
} | |||
最後に以下のコマンドでvendor/autoload.phpを更新。 | |||
composer dump-autoload | |||
==== Getting Started with PHPUnit 9 ==== | ==== Getting Started with PHPUnit 9 ==== | ||
| 555行目: | 1,323行目: | ||
phpunit tests | phpunit tests | ||
==== | ====2. Writing Tests for PHPUnit==== | ||
出典: [https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual]。 | 出典: [https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual]。 | ||
| 600行目: | 1,368行目: | ||
<nowiki> </nowiki> } | <nowiki> </nowiki> } | ||
} | } | ||
命名規則があっていないと、以下のメッセージが出てphpunitの実行に失敗する。 | |||
Class <Class>Test could not be found in /path/to/<Class>Test.php | |||
後述するが、メソッドテスト時は、data providerでテストデータを与えて、1テストメソッドで1試験対象メソッドをテストするといい。 | |||
=====Depends===== | =====Depends===== | ||
前回の試験で準備した結果を利用したい場合、@dependsのアノテーションでテスト関数を指定しておくと、指定したテスト関数のreturnを引数に受け継いだテスト関数を記述できる。 | 前回の試験で準備した結果を利用したい場合、@dependsのアノテーションでテスト関数を指定しておくと、指定したテスト関数のreturnを引数に受け継いだテスト関数を記述できる。 | ||
| 605行目: | 1,377行目: | ||
@dependsは複数指定でき、指定順に事前に試験が実行されて引数に渡される。 | @dependsは複数指定でき、指定順に事前に試験が実行されて引数に渡される。 | ||
=====Data Provider===== | =====Data Provider===== | ||
====== About ====== | |||
* [https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#data-providers 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual] | |||
* https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83 | |||
ある関数に対して、テストケースを用意して、複数の引数の組み合わせを試験したいことがよくある。こういうときのために、テスト関数に渡す引数を生成する関数のデータプロバイダーを指定できる。@dataProviderで関数を指定する。データプロバイダーは引数のリストを配列で返すようにする。 | ある関数に対して、テストケースを用意して、複数の引数の組み合わせを試験したいことがよくある。こういうときのために、テスト関数に渡す引数を生成する関数のデータプロバイダーを指定できる。@dataProviderで関数を指定する。データプロバイダーは引数のリストを配列で返すようにする。 | ||
データプロバイダーを使うことで、テスト用のメソッドは1個で、データだけ変えてテストできる。1メソッド1テストメソッドで対応できてスマート。また、データプロバイダーを使うと、どのデータで失敗したかがわかる。 | |||
<?php declare(strict_types=1); | |||
use PHPUnit\Framework\TestCase; | |||
final class DataTest extends TestCase | |||
{ | |||
/** | |||
* @dataProvider additionProvider | |||
*/ | |||
public function testAdd(int $a, int $b, int $expected): void | |||
{ | |||
$this->assertSame($expected, $a + $b); | |||
} | |||
public function additionProvider(): array | |||
{ | |||
return [ | |||
[0, 0, 0], | |||
[0, 1, 1], | |||
[1, 0, 1], | |||
[1, 1, 3] | |||
]; | |||
} | |||
} | |||
data providerの使用方法。 | |||
# テストデータを配列かIteratorで返却するpublic メソッド (data provider) を用意 | |||
# テストメソッドの引数で、data providerの配列を受け付ける。 | |||
# data providerを使用したいテストメソッドで、@dataProvider <nowiki><data provider method> のアノテーションを指定する。</nowiki> | |||
データセットはリストでもいいが、連想配列でキーにテスト名を書くとわかりやすい。 | |||
<?php declare(strict_types=1); | |||
use PHPUnit\Framework\TestCase; | |||
final class DataTest extends TestCase | |||
{ | |||
/** | |||
* @dataProvider additionProvider | |||
*/ | |||
public function testAdd(int $a, int $b, int $expected): void | |||
{ | |||
$this->assertSame($expected, $a + $b); | |||
} | |||
public function additionProvider(): array | |||
{ | |||
return [ | |||
'adding zeros' => [0, 0, 0], | |||
'zero plus one' => [0, 1, 1], | |||
'one plus zero' => [1, 0, 1], | |||
'one plus one' => [1, 1, 3] | |||
]; | |||
} | |||
} | |||
テストデータに意味がある場合は、記載した方がよさそう。このテストデータ部分にMock用のデータを渡したりもできる。 | |||
Mockでデータに応じて処理を変える場合も、データプロバイダーのデータでif文などの条件を入れたり、mock_dataのような専用のキーを渡す形にすればいい。 | |||
データ数が多い場合、名前付き配列にしておくと、どういうデータ項目で失敗したかがわかりやすい。Iteratorオブジェクトを返してもいい。 | |||
====== Exception ====== | |||
テストデータによって、例外が発生することを期待する場合、いくつか方法がある。テストデータにthrowsException:boolの列を用意して、それで判定する。または、例外用のテストメソッドを用意してそちらにするというのもある。複雑なら後者、テストケースがそんなに多くなくてシンプルなら前者でいいと思う。 | |||
/** | |||
* @dataProvider provideCases | |||
*/ | |||
public function testSomething($input, $expected, $throwsException) | |||
{ | |||
if ($throwsException) { | |||
$this->expectException(\InvalidArgumentException::class); | |||
} | |||
$result = $this->target->doSomething($input); | |||
if (!$throwsException) { | |||
$this->assertSame($expected, $result); | |||
} | |||
} | |||
public static function provideCases(): array | |||
{ | |||
return [ | |||
'正常系' => ['input' => 10, 'expected' => 100, 'throwsException' => false], | |||
'異常系' => ['input' => -1, 'expected' => null, 'throwsException' => true], | |||
]; | |||
} | |||
====== Naming ====== | |||
data providerメソッドの命名。「複数メソッド共通: provideXxx」にする。 | |||
「特定メソッド専用: testXxxDataProvider」にすると、testメソッドと対応が同じでわかりやすいのだが、testから始まると、testメソッド扱いされるので、assertがないと警告が出る。命名規則的にはxxxxTest/xxxProviderがきれいなんだけど、test/provideで使う場所と隣同士にすれば、そんなに困ることはないだろう。 | |||
他に、返却する配列の順番。公式だと、引数→期待の順番。だが、引数は複数あり得るのだから、期待→引数の順番がわかりやすい。assertの順番ともあうし。 | |||
メソッドは、test→provideの順番に書くといいみたい。testメソッドに対して、テストケースのデータを流すから。メソッドが先で、データが後。どういう試験をするかが、メソッドで先にわかるイメージの模様。 | |||
テストメソッドの名前は、test<nethod name><テスト観点> みたいな感じにすると良い。次のResolutionのように、1メソッドに対して、違うテスト観点で複数テストすることがあるから。 | |||
====== Resolution ====== | |||
https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83 | |||
data providerを使って渡すテストデータは、同じ観点にする。 | |||
例えば、表示の試験は、表示のバリエーション。表示の他に、ボタンの動作試験をしたい場合、違うテストメソッド、data providerにしたらしい。 | |||
1個のdata providerで複数の観点の試験もできなくはないが、引数とテストメソッドが複雑になって、わかりにくくなる。 | |||
===== 3. The Command-Line Test Runner ===== | ===== 3. The Command-Line Test Runner ===== | ||
| 617行目: | 1,496行目: | ||
phpunit <file> | phpunit <file> | ||
phpunit <directory> | phpunit <directory> | ||
引数にテスト対象ファイルの相対パスか、ディレクトリーを指定する。ディレクトリーを指定した場合、配下の全*Test.phpを実行する。 | 引数にテスト対象ファイルの相対パスか、ディレクトリーを指定する。ディレクトリーを指定した場合、配下の全*Test.phpを実行する。 | ||
| 632行目: | 1,507行目: | ||
*setUp/tearDown: テストメソッド単位の前後処理。テスト対象インスタンスの生成など。tearDownは何もしなくてもいいことが多い。 | *setUp/tearDown: テストメソッド単位の前後処理。テスト対象インスタンスの生成など。tearDownは何もしなくてもいいことが多い。 | ||
*setUpBeforeClass/tearDownAfterClass: クラス単位の前後処理。 DB接続など。 | *setUpBeforeClass/tearDownAfterClass: クラス単位の前後処理。 DB接続など。 | ||
=====XML Configuration File | |||
====== 共通クラスの初期化処理 ====== | |||
https://chatgpt.com/share/6836d32d-e9b8-800b-9433-444ff1a1e2bf | |||
PHPUnitのTestCaseを継承した独自の共通処理クラスを作る場合。初期化処理は__constructでやってはいけないらしい。 | |||
テスト用にいろいろ特別な初期化をしているから。 | |||
代わりに、setUpで共通で使うインスタンス生成などをする。子クラスではparent::setUp()が毎回必要になるがしかたない。 | |||
==== XML Configuration File ==== | |||
出典: | 出典: | ||
*[https://docs.phpunit.de/en/9.6/organizing-tests.html 5. Organizing Tests — PHPUnit 9.6 Manual] | *[https://docs.phpunit.de/en/9.6/organizing-tests.html 5. Organizing Tests — PHPUnit 9.6 Manual] | ||
*[https://docs.phpunit.de/en/9.6/configuration.html 3. The XML Configuration File — PHPUnit 9.6 Manual] | *[https://docs.phpunit.de/en/9.6/configuration.html 3. The XML Configuration File — PHPUnit 9.6 Manual] | ||
基本はコマンドでテスト対象クラス・ファイルを指定してテストを実行する。他に、XMLの設定ファイル (phpunit.xml) でもテスト対象を指定できる。 | 基本はコマンドでテスト対象クラス・ファイルを指定してテストを実行する。他に、XMLの設定ファイル (phpunit.xml) でもテスト対象を指定できる。 | ||
ただし、phpunit.xmlかphpunit.xml.distがあって、かつ--configurationの指定がない場合、これらのファイルを自動的に読み込む。 | |||
testsディレクトリーの全*Test.phpを対象にする最小限の例は以下。 | testsディレクトリーの全*Test.phpを対象にする最小限の例は以下。 | ||
| 646行目: | 1,533行目: | ||
</testsuites> | </testsuites> | ||
</phpunit> | </phpunit> | ||
phpunit.xmlがあれば、単にphpunitコマンドを実行するだけでいい。 | |||
phpunit --bootstrap src/autoload.php --testsuite money | phpunit --bootstrap src/autoload.php --testsuite money | ||
上記のコマンド相当になる。<syntaxhighlight lang="xml"> | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<phpunit bootstrap="vendor/autoload.php" colors="true"> | |||
<testsuites> | |||
<testsuite name="tests"> | |||
<directory>tests</directory> | |||
</testsuite> | |||
</testsuites> | |||
</phpunit> | |||
</syntaxhighlight> | |||
https://chatgpt.com/share/6821bc52-3324-800b-a98c-cbefe6c42324 | |||
{| class="wikitable" | |||
|+ | |||
!element | |||
!attribute | |||
!default | |||
! | |||
|- | |||
|phpunit | |||
|bootstrap | |||
| - | |||
| --bootstrap相当。テスト実行前の読込スクリプト。 | |||
|- | |||
| | |||
|colors | |||
|false | |||
|true=--colors=auto相当、false=--colors=never相当。 | |||
|- | |||
| | |||
|verbose | |||
| | |||
| | |||
|- | |||
|testsuites | |||
| - | |||
| - | |||
|testsuiteの親要素。 | |||
|- | |||
|testsuite | |||
| - | |||
| - | |||
|name属性が必須で、テスト検索用の1以上のdirectoryかfile要素が必要。 | |||
|- | |||
|testsuite | |||
|name | |||
| | |||
|必須。ディレクトリー名でいい気がする。 | |||
|- | |||
|phpunit/php | |||
| | |||
| | |||
|PHPの設定。 | |||
|- | |||
|phpunit/php/includePath | |||
| | |||
| | |||
|include_pathの先頭に追加するパス。 | |||
|- | |||
|phpunit/php/ini | |||
|name/value | |||
| | |||
| | |||
|- | |||
|phpunit/php/const, var | |||
| | |||
| | |||
|グローバルな定数、変数を設定。 | |||
|- | |||
|phpunit/php/env | |||
|name/value | |||
| | |||
|環境変数。 | |||
|- | |||
|phpunit/php/get, post, cookie, server, files, request | |||
|name/value | |||
| | |||
|該当するスーパーグローバル変数の設定。 | |||
|} | |||
GuzzleのHTTPクライアントでプロキシー設定無視で以下の設定はよく使うかもしれない。 | |||
<php><env name="NO_PROXY" value="*"/></php> | |||
===== bootstrap ===== | |||
https://chatgpt.com/share/6821bc52-3324-800b-a98c-cbefe6c42324 | |||
autoloadを使っていないプロジェクトだと、手動でプロジェクトルートパスなどの指定が必要になる。 | |||
tests以下にbootstrap.phpを用意して、その中で必要なものを読み込む形にするといい。 | |||
<?php | |||
// tests/bootstrap.php | |||
// $additionalPath = PROJECT_ROOT . '/src'; set_include_path(get_include_path() . PATH_SEPARATOR . $additionalPath); set_include_path(implode(PATH_SEPARATOR, [ get_include_path(), PROJECT_ROOT . '/src', PROJECT_ROOT . '/lib' ])); | |||
// Composerのオートローダー | |||
require_once __DIR__ . '/../vendor/autoload.php'; | |||
// オートローダーでは読み込めない独自ファイル | |||
require_once __DIR__ . '/manual-loads/special-loader.php'; | |||
// 必要なら定数定義 | |||
define('PROJECT_ROOT', dirname(__DIR__)); | |||
<!-- phpunit.xml --> | |||
<phpunit bootstrap="tests/bootstrap.php"> | |||
====Assertions==== | ====Assertions==== | ||
===== About ===== | |||
Ref: | Ref: | ||
*[https://docs.phpunit.de/en/9.6/assertions.html 1. Assertions — PHPUnit 9.6 Manual]. | *[https://docs.phpunit.de/en/9.6/assertions.html 1. Assertions — PHPUnit 9.6 Manual]. | ||
*[https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html?highlight=assertSame#testing-exceptions 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual] | *[https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html?highlight=assertSame#testing-exceptions 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual] | ||
基本はassertSameでテストすればいいのだが、それ以外にも例外とかいろいろ試験したいケースがあるので、メソッドを整理する。 | 基本はassertSameでテストすればいいのだが、それ以外にも例外とかいろいろ試験したいケースがあるので、メソッドを整理する。 | ||
===== Method ===== | |||
インスタンスメソッドとstaticメソッドの2種類がある。他に、グローバル関数もある。どれを使ってもいい。 | |||
* アサーションメソッド: PHPUnit\Framework\Assert | |||
* PHPUnit\Framework\TestCaseはAssertを継承している。 | |||
* phpunit/phpunit/src/Framework/Assert/Functions.php でグローバル関数。 | |||
入力文字数や好みの問題。グローバル関数は内部的にstaticメソッドを呼んでいる。 | |||
TestCaseのインスタンスメソッドの方が違和感がないという説がある。 | |||
$this->よりもself::のほうが短い。グローバル関数は名前空間がないのがまずい。self::のstaticメソッドでいい気がする。 | |||
===== Negate ===== | |||
assert系メソッドは、assertNotでNotを前置したら否定形になる。assertStringとかグループがある場合は、assertStringNotのように、グループの後にNotになる。 | |||
===== 同値判定 ===== | |||
* assertSame/assertNotSame: 型と値の両方を検査 (===相当)。 | |||
* assertEquals: 値を検査 (==相当)。 | |||
オブジェクトの属性値の値が同じかどうかをみたいなら、assertEqualsになる。assertSameは参照 (ポインター) のアドレス値の比較みたいなことをするから。 | |||
基本はassertSameでよいと思われる。 | |||
* assertEqualsCanonicalizing() | |||
* assertEqualsIgnoringCase() | |||
* assertEqualsWithDelta() | |||
他にこういうバリエーションがある。 | |||
===== 論理判定 ===== | |||
* assertIsBool | |||
* assertFalse | |||
* assertTrue() | |||
===== 数値判定 ===== | |||
* assertGreaterThan | |||
* assertGreaterThanOrEqual | |||
* assertLessThan | |||
* assertLessThanOrEqual | |||
* assertInfinite | |||
* assertNan | |||
* assertIsInt | |||
* assertIsNumeric | |||
* assertIsFloat | |||
===== 文字列判定 ===== | |||
* assertIsString | |||
* assertMatchesRegularExpression | |||
* assertStringContainsString | |||
* assertStringContainsStringIgnoringCase() | |||
* assertStringMatchesFormat | |||
* assertStringMatchesFormatFile | |||
* assertStringEndsWith | |||
* assertStringEqualsFile | |||
* assertStringStartsWith | |||
===== 配列判定 ===== | |||
配列の検査用のメソッドがある。 | |||
* 要素数 | |||
** assertEmpty | |||
** assertSameSize(): 2個の配列の要素数が同じか。 | |||
** assertCount(): 指定した配列の要素数が指定数か。データの取得数などでこちらをよく使いそう。 | |||
* 包含 | |||
** assertContains | |||
** assertContainsOnly() | |||
** assertContainsOnlyInstancesOf() | |||
** assertArrayHasKey | |||
* assertIsArray | |||
* assertIsIterable | |||
===== クラス ===== | |||
* assertClassHasAttribute | |||
* assertClassHasStaticAttribute | |||
* assertObjectEquals | |||
* assertInstanceOf | |||
* assertIsCallable | |||
* assertIsObject | |||
* assertIsResource | |||
* assertIsScalar | |||
* assertNull | |||
* assertObjectHasProperty | |||
===== ファイル ===== | |||
* assertDirectoryExists | |||
* assertDirectoryIsReadable | |||
* assertDirectoryIsWritable | |||
* assertFileEquals | |||
* assertFileExists | |||
* assertFileIsReadable | |||
* assertFileIsWritable | |||
* assertIsReadable | |||
* assertIsWritable | |||
===== その他 ===== | |||
* assertThat | |||
* assertJsonFileEqualsJsonFile | |||
* assertJsonStringEqualsJsonFile | |||
* assertJsonStringEqualsJsonString | |||
* assertXmlFileEqualsXmlFile | |||
* assertXmlStringEqualsXmlFile | |||
* assertXmlStringEqualsXmlString | |||
=====Exception===== | =====Exception===== | ||
[https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#testing-exceptions 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual] | |||
特に例外の試験がイレギュラー。 | 特に例外の試験がイレギュラー。 | ||
<?php declare(strict_types=1); | <?php declare(strict_types=1); | ||
| 672行目: | 1,780行目: | ||
*expectExceptionMessageMatches: | *expectExceptionMessageMatches: | ||
例外が発生する処理の前に記述しておく。 | 例外が発生する処理の前に記述しておく。 | ||
=====Output===== | =====Testing Output===== | ||
[https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#testing-output 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual] | |||
echoなど標準出力を試験する際も専用のメソッドがある。 | echoなど標準出力を試験する際も専用のメソッドがある。 | ||
*<code>void expectOutputRegex(string $regularExpression)</code> | *<code>void expectOutputRegex(string $regularExpression)</code> | ||
| 679行目: | 1,789行目: | ||
*<code>string getActualOutput()</code> | *<code>string getActualOutput()</code> | ||
expectExceptionと同様に事前にセットしておく。 | expectExceptionと同様に事前にセットしておく。 | ||
===== Constraint ===== | |||
[https://docs.phpunit.de/en/9.6/assertions.html#appendixes-assertions-assertthat-tables-constraints 1. Assertions — PHPUnit 9.6 Manual] | |||
assertThatとwithで使えるConstraint。通常のassertと似たような名前のものも多いので注意する。equalToとかはConstraintにしかない。PHPUnit\Framework\Constraintの名前空間の各種クラスのメソッドになる。 | |||
* 論理 | |||
** isFalse | |||
** isTrue | |||
** logicalAnd | |||
** logicalNot | |||
** logicalOr | |||
** logicalXor | |||
* 数値 | |||
** greaterThan | |||
** greaterThanOrEqual | |||
** lessThan | |||
** lessThanOrEqual | |||
* 文字列 | |||
** matchesRegularExpression | |||
** stringContains | |||
** stringEndsWith | |||
** stringStartsWith | |||
* 配列 | |||
** arrayHasKey | |||
** contains | |||
** containsOnly | |||
** containsOnlyInstanceOf | |||
* クラス | |||
** classHasAttribute | |||
** classHasStaticAttribute | |||
** objectHasAttribute | |||
** isInstanceOf | |||
** isNull | |||
* ファイル | |||
** directorExists | |||
** fileExists | |||
** isReadable | |||
** isWritable | |||
* その他 | |||
** isType | |||
** anything | |||
** equalTo | |||
** identicalTo | |||
上記にない制約は$this->callbackで自分で定義する。引数に引数がきて、true/falseを返す。 | |||
{| class="wikitable" | |||
|+Table 1.1 Constraints | |||
!Constraint | |||
!Meaning | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\IsAnything anything()</code> | |||
|Constraint that accepts any input value. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\ArrayHasKey arrayHasKey(mixed $key)</code> | |||
|Constraint that asserts that the array has a given key. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\TraversableContains contains(mixed $value)</code> | |||
|Constraint that asserts that the <code>array</code> or object that implements the <code>Iterator</code> interface contains a given value. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\TraversableContainsOnly containsOnly(string $type)</code> | |||
|Constraint that asserts that the <code>array</code> or object that implements the <code>Iterator</code> interface contains only values of a given type. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\TraversableContainsOnly containsOnlyInstancesOf(string $classname)</code> | |||
|Constraint that asserts that the <code>array</code> or object that implements the <code>Iterator</code> interface contains only instances of a given classname. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\IsEqual equalTo($value, $delta = 0, $maxDepth = 10)</code> | |||
|Constraint that checks if one value is equal to another. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\DirectoryExists directoryExists()</code> | |||
|Constraint that checks if the directory exists. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\FileExists fileExists()</code> | |||
|Constraint that checks if the file(name) exists. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\IsReadable isReadable()</code> | |||
|Constraint that checks if the file(name) is readable. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\IsWritable isWritable()</code> | |||
|Constraint that checks if the file(name) is writable. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\GreaterThan greaterThan(mixed $value)</code> | |||
|Constraint that asserts that the value is greater than a given value. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\LogicalOr greaterThanOrEqual(mixed $value)</code> | |||
|Constraint that asserts that the value is greater than or equal to a given value. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\ClassHasAttribute classHasAttribute(string $attributeName)</code> | |||
|Constraint that asserts that the class has a given attribute. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\ClassHasStaticAttribute classHasStaticAttribute(string $attributeName)</code> | |||
|Constraint that asserts that the class has a given static attribute. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\ObjectHasAttribute objectHasAttribute(string $attributeName)</code> | |||
|Constraint that asserts that the object has a given attribute. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\IsIdentical identicalTo(mixed $value)</code> | |||
|Constraint that asserts that one value is identical to another. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\IsFalse isFalse()</code> | |||
|Constraint that asserts that the value is <code>false</code>. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\IsInstanceOf isInstanceOf(string $className)</code> | |||
|Constraint that asserts that the object is an instance of a given class. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\IsNull isNull()</code> | |||
|Constraint that asserts that the value is <code>null</code>. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\IsTrue isTrue()</code> | |||
|Constraint that asserts that the value is <code>true</code>. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\IsType isType(string $type)</code> | |||
|Constraint that asserts that the value is of a specified type. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\LessThan lessThan(mixed $value)</code> | |||
|Constraint that asserts that the value is smaller than a given value. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\LogicalOr lessThanOrEqual(mixed $value)</code> | |||
|Constraint that asserts that the value is smaller than or equal to a given value. | |||
|- | |||
|<code>logicalAnd()</code> | |||
|Logical AND. | |||
|- | |||
|<code>logicalNot(PHPUnit\Framework\Constraint $constraint)</code> | |||
|Logical NOT. | |||
|- | |||
|<code>logicalOr()</code> | |||
|Logical OR. | |||
|- | |||
|<code>logicalXor()</code> | |||
|Logical XOR. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\PCREMatch matchesRegularExpression(string $pattern)</code> | |||
|Constraint that asserts that the string matches a regular expression. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\StringContains stringContains(string $string, bool $case)</code> | |||
|Constraint that asserts that the string contains a given string. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\StringEndsWith stringEndsWith(string $suffix)</code> | |||
|Constraint that asserts that the string ends with a given suffix. | |||
|- | |||
|<code>PHPUnit\Framework\Constraint\StringStartsWith stringStartsWith(string $prefix)</code> | |||
|Constraint that asserts that the string starts with a given prefix. | |||
|} | |||
====Command-Line==== | ====Command-Line==== | ||
Ref: [https://docs.phpunit.de/en/9.6/textui.html 3. The Command-Line Test Runner — PHPUnit 9.6 Manual]. | Ref: [https://docs.phpunit.de/en/9.6/textui.html 3. The Command-Line Test Runner — PHPUnit 9.6 Manual]. | ||
| 685行目: | 1,939行目: | ||
*phpunit file.php: 指定したファイルのテストを実行。 | *phpunit file.php: 指定したファイルのテストを実行。 | ||
*--testsuite <name>: テストを指定。 | *--testsuite <name>: テストを指定。 | ||
====Test Doubles==== | ====8. Test Doubles==== | ||
Ref: [https://docs.phpunit.de/en/9.6/test-doubles.html 8. Test Doubles — PHPUnit 9.6 Manual]. | Ref: [https://docs.phpunit.de/en/9.6/test-doubles.html 8. Test Doubles — PHPUnit 9.6 Manual]. | ||
===== About ===== | |||
テスト時に、依存関係を模擬したもので置換したいことがある。PHPUnitにそういう仕組が用意されている。 | テスト時に、依存関係を模擬したもので置換したいことがある。PHPUnitにそういう仕組が用意されている。 | ||
stub= | 「[https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%B9%E3%83%88%E3%83%80%E3%83%96%E3%83%AB テストダブル - Wikipedia]」Doubleは代役、影武者を意味する。ロックマンX4のダブルはたぶんこの英単語が由来だろう。 | ||
テスト対象クラスのプロパティーのクラスやメソッドの代用品をテストダブルと呼ぶ。テストダブルで置換するというような言葉の使い方をすると思われる。 | |||
テストダブルには、stubとmockがある。 | |||
* Stub: SUT内メソッド戻り値の検証 | |||
* Mock: SUT内メソッド引数の検証。 | |||
メソッド内で他のクラス・メソッドを使う場合はmockで対象クラスを模擬させる。 | |||
PHPUnitではテストダブル用に3の基本APIがある。 | |||
* createStub | |||
* createMock | |||
* getMockBuilder | |||
引数に置換対象のクラスを指定してインスタンスを作成する。createStubとcreateMockは対象を全部置換する。getMockBuilderはこれら2種のメソッドを含んでいて、置換するメソッド・プロパティーを自分で選択できる。特定メソッドだけを置換して、他はそのまま使いたい場合、これを使うしかない。 | |||
===== Stub ===== | |||
オブジェクトのメソッドの戻り値を、テストダブルに置換する手法をスタブと呼ぶ。メソッド内の特定メソッドの戻り値を模擬することで、テストと関係ない処理の影響を無視できる。 | |||
実際に使う際は、テスト対象クラスのインスタンス作成後に、インスタンスがのプロパティーに設定して、インスタンス内の別クラスメソッド呼び出しを模擬する。 | |||
<?php declare(strict_types=1); | |||
class SomeClass | |||
{ | |||
public function doSomething() | |||
{ | |||
// Do something. | |||
} | |||
} | |||
<?php declare(strict_types=1); | |||
use PHPUnit\Framework\TestCase; | |||
final class StubTest extends TestCase | |||
{ | |||
public function testStub(): void | |||
{ | |||
// Create a stub for the SomeClass class. | |||
$stub = $this->createStub(SomeClass::class); | |||
// Configure the stub. | |||
$stub->method('doSomething') | |||
->willReturn('foo'); | |||
// Calling $stub->doSomething() will now return | |||
// 'foo'. | |||
$this->assertSame('foo', $stub->doSomething()); | |||
} | |||
} | |||
単に、メソッドの戻り値を模擬したいだけなら、expectsとwithは不要。method()->willReturnだけで十分。 | |||
{| class="wikitable" | |||
|+Table 8.1 Stubbing short hands | |||
!short hand | |||
!longer syntax | |||
|- | |||
|<code>willReturn($value)</code> | |||
|<code>will($this->returnValue($value))</code> | |||
|- | |||
|<code>willReturnArgument($argumentIndex)</code> | |||
|<code>will($this->returnArgument($argumentIndex))</code> | |||
|- | |||
|<code>willReturnCallback($callback)</code> | |||
|<code>will($this->returnCallback($callback))</code> | |||
|- | |||
|<code>willReturnMap($valueMap)</code> | |||
|<code>will($this->returnValueMap($valueMap))</code> | |||
|- | |||
|<code>willReturnOnConsecutiveCalls($value1, $value2)</code> | |||
|<code>will($this->onConsecutiveCalls($value1, $value2))</code> | |||
|- | |||
|<code>willReturnSelf()</code> | |||
|<code>will($this->returnSelf())</code> | |||
|- | |||
|<code>willThrowException($exception)</code> | |||
|<code>will($this->throwException($exception))</code> | |||
|}willReturnCallbackで呼び出し関数をまるごと別のものに置換できる。これが非常に便利。 | |||
引数を指定したい場合、with(引数)->willReturn()がシンプル。ただし、この方法は最後に指定した引数1個しか対応できない。引数違いで戻り値を模擬したいなら、willReturnMapかwillReturnCallbackで、引数に応じた戻り値を設定する。 | |||
willReturnCallbackのcallbackに渡される引数の記載はないが、モック対象メソッドに渡される引数が、その順番で渡される動きになっている。 | |||
===== Mock Objects ===== | |||
https://chatgpt.com/share/683e72cc-461c-800b-a7fc-5e8b417a44aa | |||
メソッドが呼び出されたかどうかを検証するための、テストダブルへの置換をモッキングと呼ぶ。 | |||
テスト対象メソッド内で、特定メソッドの呼び出しを検査できる。MVCでViewへの値渡しなんかの検査にうってつけ。 | |||
モックオブジェクトにはテストスタブの機能も含んでいる。が、モッキングのための専用処理があるので、インスタンス生成時などのコストがやや大きい。理由がなければ、Stubを使った方がいい。 | |||
判断基準として、expects/withを使うならMock、そうじゃなければStubのようなイメージ。<syntaxhighlight lang="php"> | |||
<?php declare(strict_types=1); | |||
use PHPUnit\Framework\TestCase; | |||
class Subject | |||
{ | |||
protected $observers = []; | |||
protected $name; | |||
public function __construct($name) | |||
{ | |||
$this->name = $name; | |||
} | |||
public function getName() | |||
{ | |||
return $this->name; | |||
} | |||
public function attach(Observer $observer) | |||
{ | |||
$this->observers[] = $observer; | |||
} | |||
public function doSomething() | |||
{ | |||
// Do something. | |||
// ... | |||
// Notify observers that we did something. | |||
$this->notify('something'); | |||
} | |||
public function doSomethingBad() | |||
{ | |||
foreach ($this->observers as $observer) { | |||
$observer->reportError(42, 'Something bad happened', $this); | |||
} | |||
} | |||
protected function notify($argument) | |||
{ | |||
foreach ($this->observers as $observer) { | |||
$observer->update($argument); | |||
} | |||
} | |||
// Other methods. | |||
} | |||
class Observer | |||
{ | |||
public function update($argument) | |||
{ | |||
// Do something. | |||
} | |||
public function reportError($errorCode, $errorMessage, Subject $subject) | |||
{ | |||
// Do something | |||
} | |||
// Other methods. | |||
} | |||
</syntaxhighlight> | |||
<syntaxhighlight lang="php"> | |||
<?php declare(strict_types=1); | <?php declare(strict_types=1); | ||
use PHPUnit\Framework\TestCase; | use PHPUnit\Framework\TestCase; | ||
| 725行目: | 2,137行目: | ||
</syntaxhighlight>基本的な作り。 | </syntaxhighlight>基本的な作り。 | ||
#createMock(<class>::class)で該当クラスのモックを作成。 | #createMock(<class>::class)で該当クラスのモックを作成。 | ||
# | #expectsに呼出回数条件のオブジェクトをセット。これはなくてもいい。 | ||
#methodで対象メソッドを指定。 | #methodで対象メソッドを指定。 | ||
# | #withで該当メソッドの引数の検証条件を指定。 | ||
#willReturnなどで戻り値を指定。 | |||
デフォルトで模擬実装はnullを返す。戻り値を変更したければ、will($this->returnValue())などを指定する。よく使うので短縮記法もある。 | デフォルトで模擬実装はnullを返す。戻り値を変更したければ、will($this->returnValue())などを指定する。よく使うので短縮記法もある。 | ||
===== Matcher ===== | |||
使えるメソッド。[https://github.com/sebastianbergmann/phpunit/blob/main/src/Framework/TestCase.php#L959<nowiki>] あたりからの一連のメソッドと思われる。</nowiki> | |||
* any | |||
* never | |||
* atLeast | |||
* once | |||
* exactly (旧at) | |||
* atMost | |||
{| class="wikitable" | {| class="wikitable" | ||
|+Table 8. | |+Table 8.2 Matchers | ||
! | !Matcher | ||
! | !Meaning | ||
|- | |- | ||
|<code> | |<code>PHPUnit\Framework\MockObject\Matcher\AnyInvokedCount any()</code> | ||
| | |Returns a matcher that matches when the method it is evaluated for is executed zero or more times. | ||
|- | |- | ||
|<code> | |<code>PHPUnit\Framework\MockObject\Matcher\InvokedCount never()</code> | ||
| | |Returns a matcher that matches when the method it is evaluated for is never executed. | ||
|- | |- | ||
|<code> | |<code>PHPUnit\Framework\MockObject\Matcher\InvokedAtLeastOnce atLeastOnce()</code> | ||
| | |Returns a matcher that matches when the method it is evaluated for is executed at least once. | ||
|- | |- | ||
|<code> | |<code>PHPUnit\Framework\MockObject\Matcher\InvokedCount once()</code> | ||
| | |Returns a matcher that matches when the method it is evaluated for is executed exactly once. | ||
|- | |- | ||
|<code> | |<code>PHPUnit\Framework\MockObject\Matcher\InvokedCount exactly(int $count)</code> | ||
|<code> | |Returns a matcher that matches when the method it is evaluated for is executed exactly <code>$count</code> times. | ||
|- | |- | ||
|<code> | |<code>PHPUnit\Framework\MockObject\Matcher\InvokedAtIndex at(int $index)</code> | ||
|<code> | |Returns a matcher that matches when the method it is evaluated for is invoked at the given <code>$index</code>. | ||
|} | |||
==== | |||
===== Mock builder ===== | |||
https://chatgpt.com/share/68424beb-e544-800b-9292-b79f89cf412d | |||
createStubやcreateMockは対象クラスを全部置換する。一部だけ置換したり、もともと存在しないメソッドを追加したり、複雑なことをしたい場合、getMockBuilderで取得する、Mock Builderを使う必要がある。メソッド一覧は以下となる。 | |||
* <code>onlyMethods(array $methods)</code>Mock Builderオブジェクトで呼び出すことで、設定可能なテストダブルに置き換えるメソッドを指定できます。他のメソッドの動作は変更されません。各メソッドは、指定されたモッククラス内に存在している必要があります。 | |||
* <code>addMethods(array $methods)</code>Mock Builderオブジェクトで呼び出すことで、指定されたモッククラスに(まだ)存在しないメソッドを指定できます。他のメソッドの動作は同じです。 | |||
* <code>setMethodsExcept(array $methods)</code>Mock Builderオブジェクトで を呼び出すことで、他のすべてのパブリックメソッドを置き換えながら、設定可能なテストダブルに置き換えないメソッドを指定できます。これは の逆の動作をします<code>onlyMethods()</code>。 | |||
* <code>setConstructorArgs(array $args)</code>元のクラスのコンストラクターに渡されるパラメーター配列を提供するために呼び出すことができます (デフォルトではダミー実装に置き換えられません)。 | |||
* <code>setMockClassName($name)</code>生成されたテストダブルクラスのクラス名を指定するために使用できます。 | |||
* <code>disableOriginalConstructor()</code>元のクラスのコンストラクターへの呼び出しを無効にするために使用できます。 | |||
* <code>disableOriginalClone()</code>元のクラスのクローンコンストラクターの呼び出しを無効にするために使用できます。 | |||
* <code>disableAutoload()__autoload()</code>テストダブルクラスの生成中に無効にするために使用できます。 | |||
use PHPUnit\Framework\TestCase; | |||
class MyClassTest extends TestCase | |||
{ | |||
public function testAddColumnAddsNewColumn() | |||
{ | |||
// fetchDataだけをスタブ化する | |||
$stub = $this->getMockBuilder(MyClass::class) | |||
->onlyMethods(['fetchData']) // ← ここでfetchDataだけモック化 | |||
->getMock(); | |||
// fetchDataが返す想定の配列をセット | |||
$stub->method('fetchData')->willReturn([ | |||
['id' => 1, 'name' => 'Alice'], | |||
['id' => 2, 'name' => 'Bob'], | |||
]); | |||
// addColumnを呼び出してテスト | |||
$result = $stub->addColumn(); | |||
// 検証 | |||
$this->assertEquals('value', $result[0]['new_column']); | |||
$this->assertEquals('value', $result[1]['new_column']); | |||
} | |||
} | |||
上記のように、最終的にgetMock()でインスタンスを取得して、methodなどで戻り値を設定する。 | |||
===== MVC ===== | |||
https://chatgpt.com/share/68395ad2-678c-800b-b27a-19c58e3a3a0a | |||
MVC系のアプリで、Viewにセットする値・変数をチェックしたいことがある。 | |||
この場合、ControllerでViewにセットしているメソッドを試験する。 | |||
$controller-><view>->assign() | |||
$controller->setApp() | |||
これらのメソッドを、expects/withで引数を検査する。willReturnはいらない。assert系のメソッドも呼ばなくていい。expects/withで引数の検査をするイメージ。 | |||
===== expects ===== | |||
https://chatgpt.com/share/682e8fd6-376c-800b-a01e-faa832b0fd07 | |||
「[https://docs.phpunit.de/en/9.6/test-doubles.html 8. Test Doubles — PHPUnit 9.6 Manual]」でモックオブジェクトを作成時に、模擬したメソッドの設定時に、expectsを使っている。 | |||
このexpectsは該当メソッドが呼ばれたか、呼ばれなかったかを試験するためのもの。例えば、if文で条件分岐していて、その中で該当メソッドが呼ばれたかどうかをチェックできる。 | |||
一本道で、呼び出し有無を気にしなくていいなら、expectsは不要。if文の条件を気にしなくて、一緒に試験できるのが便利。 | |||
なお、expectsを使わない場合、同名で競合するので、methodという名前のメソッドが、モックオブジェクトにあってはいけない。ある場合、expects($this->any())でexpectsを挟む必要がある。 | |||
ただ、methodという名前のメソッドがあることは普通ないと思う。 | |||
このexpectsは「[https://github.com/sebastianbergmann/phpunit/blob/5226e323514a73151c1c7909af224c01a1bbe9aa/src/Framework/MockObject/Runtime/Interface/MockObject.php#L19 phpunit/src/Framework/MockObject/Runtime/Interface/MockObject.php at 5226e323514a73151c1c7909af224c01a1bbe9aa · sebastianbergmann/phpunit]」 | |||
interface MockObject extends Stub | |||
{ | |||
public function expects(InvocationOrder $invocationRule): InvocationStubber; | |||
} | |||
これが定義な模様。引数にはInvocationOrderをとる。InvocationOrderは「[https://github.com/sebastianbergmann/phpunit/blob/main/src/Framework/MockObject/Runtime/Rule/InvokedCount.php phpunit/src/Framework/MockObject/Runtime/Rule/InvokedCount.php at main · sebastianbergmann/phpunit]」などで継承されている。 | |||
===== with ===== | |||
モックオブジェクトのメソッドの引数の検証に使うメソッド。ソースコードは以下。 | |||
* [https://github.com/sebastianbergmann/phpunit/blob/e075df0a9d89a824ba66e12a7c23618d1a42faf3/src/Framework/MockObject/Runtime/Interface/InvocationStubber.php#L46 phpunit/src/Framework/MockObject/Runtime/Interface/InvocationStubber.php at e075df0a9d89a824ba66e12a7c23618d1a42faf3 · sebastianbergmann/phpunit] | |||
* [https://github.com/sebastianbergmann/phpunit/blob/e075df0a9d89a824ba66e12a7c23618d1a42faf3/src/Framework/MockObject/Runtime/InvocationStubberImplementation.php#L133 phpunit/src/Framework/MockObject/Runtime/InvocationStubberImplementation.php at e075df0a9d89a824ba66e12a7c23618d1a42faf3 · sebastianbergmann/phpunit] | |||
https://chatgpt.com/share/683d657e-8578-800b-8e71-b09dbce969dd | |||
「[https://docs.phpunit.de/en/9.6/test-doubles.html#mock-objects 8. Test Doubles — PHPUnit 9.6 Manual]」に記載がある。<blockquote>The <code>with()</code> method can take any number of arguments, corresponding to the number of arguments to the method being mocked. You can specify more advanced constraints on the method’s arguments than a simple match.</blockquote>モック対象メソッドの個数と同じ引数を受け取れる。 | |||
それぞれの引数の位置で、該当引数の検証条件を指定する。指定可能な内容は以下の3種類。 | |||
# リテラル値 | |||
# Constraint: 指定可能なConstraints (制約) は「[https://docs.phpunit.de/en/9.6/assertions.html#appendixes-assertions-assertthat-tables-constraints 1. Assertions — PHPUnit 9.6 Manual]」にある。 | |||
# callback: callbackの引数は、検証対象の引数で、OKならtrueを返す。 | |||
配列要素数や、複数のConstraintを組み合わせたい場合などは、callbackを使うしかない。 | |||
setAppやassignなどで、同じメソッドで1番目の引数に応じて、2番目の内容が変わる場合、工夫が必要。 | |||
expectやwithでは検証しないで、willReturnCallbackを使うしかない。連想配列に、実際に渡ってきたキー・バリューのセットを格納して、実行後にまとめてassertするか、コールバック内でassertしてチェックする。 | |||
$mock->method('assign') | |||
->willReturnCallback(function ($key, $val) { | |||
switch ($key) { | |||
case 'title': | |||
PHPUnit\Framework\Assert::assertSame('マイページ', $val); | |||
break; | |||
case 'user': | |||
PHPUnit\Framework\Assert::assertInstanceOf(User::class, $val); | |||
break; | |||
default: | |||
PHPUnit\Framework\Assert::fail("Unexpected key: $key"); | |||
} | |||
}); | |||
$assigned = []; | |||
$mock->method('assign') | |||
->willReturnCallback(function ($key, $val) use (&$assigned) { | |||
$assigned[$key] = $val; | |||
}); | |||
// テスト対象実行後にチェック | |||
$this->assertSame('マイページ', $assigned['title'] ?? null); | |||
$this->assertInstanceOf(User::class, $assigned['user'] ?? null); | |||
willReturnCallback内のifでいい気がする。複雑なら後者のスパイ風で。 | |||
willReturnCallbackでやる場合、Viewに渡すメソッドのもともとの戻り値がnullだから問題ないが、そうでない場合は、ちゃんとreturnでダミーの値を返さないと、後続の処理で不都合出るので注意する。 | |||
withで検査する場合、self::assertSame()でメソッドの検証はせずに、単にテスト対象メソッドを呼び出す。呼び出すと、withで仕込んだものが呼ばれて、中で検証するイメージになる。 | |||
===== DI ===== | |||
https://chatgpt.com/share/683faf08-f374-800b-ac40-b96b427d7365 | |||
テスト対象クラスのプロパティーのインスタンスをテストダブルに置換する場合、注意が必要。だいたい、privateになっているから、DIで渡す前に、置換対象のメソッドの模擬を設定してから、コンストラクターに渡す必要がある。 | |||
面倒くさかったら、テストダブルインスタンスの作成+メソッド設定を共通メソッドにしてもよいかもしれない。が、テストダブルの戻り値は、DB取得結果とかだと大事なので、手間だが1個ずつやった方がいいかもしれない。 | |||
protected function prepareLoggerWithLogReturn($value): LoggerInterface | |||
{ | |||
$logger = $this->createLoggerMock(); | |||
$logger->method('log')->willReturn($value); | |||
return $logger; | |||
} | |||
$logger = $this->prepareLoggerWithLogReturn(true); | |||
===== 外部static/グローバル関数の模擬 ===== | |||
https://chatgpt.com/share/68424beb-e544-800b-9292-b79f89cf412d | |||
例えば、ログインユーザーIDなど、引数に渡すまでもない共通値の取得が、SUTのメソッド内にあったりする。ただ、こういうstaticやグローバルなメソッド・関数は、PHPUnitで置換が難しい。 | |||
回避方法がいくつかある。 | |||
# DIで対象staticメソッドクラスをプロパティーに持たせる。 | |||
# 1と似た考えで、ラッパークラスを作る。 | |||
# 対象クラス内で、ラッパーメソッド (protected) を用意する。 | |||
1や2が望ましいようだが、3のラッパーメソッド用意は簡単。ひとまず3でいい。 | |||
テストのためだけに意味ない関数を追加するように見える。が、「テストできないコードはそれだけで設計に問題がある」とも言える。そういうものと思っておくと良いらしい。 | |||
====Other==== | |||
=====Test private/protected===== | =====Test private/protected===== | ||
*[https://stackoverflow.com/questions/249664/best-practices-to-test-protected-methods-with-phpunit php - Best practices to test protected methods with PHPUnit - Stack Overflow] | *[https://stackoverflow.com/questions/249664/best-practices-to-test-protected-methods-with-phpunit php - Best practices to test protected methods with PHPUnit - Stack Overflow] | ||
| 763行目: | 2,336行目: | ||
/** | /** | ||
* privateメソッドを実行する. | * privateメソッドを実行する. | ||
* @param string $ | * @param object $sut テスト対象のインスタンス。 | ||
* @param array $param | * @param string $method_name privateメソッドの名前。 | ||
* @return mixed | * @param array $param privateメソッドに渡す引数。 | ||
* @return mixed 実行結果。 | |||
* @throws \ReflectionException 引数のクラスがない場合に発生. | * @throws \ReflectionException 引数のクラスがない場合に発生. | ||
*/ | */ | ||
private function doMethod(string $ | private function doMethod(object $sut, string $method_name, array $param): mixed | ||
{ | { | ||
// ReflectionClassをテスト対象のクラスをもとに作る. | // ReflectionClassをテスト対象のクラスをもとに作る. | ||
$reflection = new \ReflectionClass($ | $reflection = new \ReflectionClass($sut); | ||
// メソッドを取得する. | // メソッドを取得する. | ||
$method = $reflection->getMethod($ | $method = $reflection->getMethod($method_name); | ||
// アクセス許可をする. | // アクセス許可をする. | ||
$method->setAccessible(true); | $method->setAccessible(true); | ||
// メソッドを実行して返却値をそのまま返す. | // メソッドを実行して返却値をそのまま返す. | ||
return $method->invokeArgs($ | return $method->invokeArgs($sut, $param); | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight>ReflectionClassを使って取得できる。上記の関数のFormController部分を試験対象のクラスに差し替えればOK。getProperty/getValueでprivateプロパティーも取得可能。 | ||
https://chatgpt.com/c/673ebadb-1638-800b-89f0-b3ed131317da | https://chatgpt.com/c/673ebadb-1638-800b-89f0-b3ed131317da | ||
| 838行目: | 2,410行目: | ||
このclientErrorをwillなどで置換すればよさそう? | このclientErrorをwillなどで置換すればよさそう? | ||
===== sut ===== | |||
https://chatgpt.com/share/682ada62-d3e0-800b-9444-db245dc44183 | |||
試験対象のクラスをメンバー変数・プロパティーに格納する。その際の名前を何にするか? | |||
instance/classとかが思いつく。対話AIによると、sutというのがいいらしい。system under testの略。テスト対象の意味。試験の専門用語っぽい。短いのでこれでいいと思う。 | |||
===== 表示内容の試験 ===== | |||
画面UIに該当文字列があるかどうかなどを試験したいことがある。 | |||
LaravelにはassertSeeというのがあるのでこれを使える。 | |||
PHPUnit自体にはない。assertStringContainsStringなど、文字列試験メソッドを使って、自分でresponseを何かで取得して評価する。こういうのは基本は機能試験で行う内容。 | |||
===== 単体試験と機能試験 ===== | |||
https://chatgpt.com/share/682ada62-d3e0-800b-9444-db245dc44183 | |||
単体試験と機能試験がある。 | |||
単体試験は、基本的にクラス単位。クラスのメソッドを試験するイメージ。 | |||
機能試験は、特定機能に関する、クラス・メソッドを試験する。1個の試験で、複数クラスを試験する違いがある。 | |||
実際のアプリ開発では、ユーザー動作や仕様の動作が重要だから、機能試験中心で問題ない気がする。 | |||
重要なところ、複雑なところ、バグの多いところをUnitTestで試験するのがいいと思う。メンテ不能なテストができてしまうのは避けたい。 | |||
* tests | |||
** Unit | |||
** Feature | |||
Unitは元ファイルのディレクトリー構成にして、Featureは機能単位。Featureで機能単位の試験を入れる。で、基本はFeatureを拡充させる。 | |||
ファイル名・クラス名。Featureの方はXXFreatureTest.phpとかにすることが多いらしい。が、せっかくディレクトリー分けている意味がないので、Test.phpでいいでしょう。 | |||
===== result cache ===== | |||
* [https://stackoverflow.com/questions/55091768/what-is-phpunit-result-cache php - What is .phpunit.result.cache - Stack Overflow] | |||
* [https://docs.phpunit.de/en/9.6/textui.html 3. The Command-Line Test Runner — PHPUnit 9.6 Manual] | |||
* [https://docs.phpunit.de/en/9.6/configuration.html 3. The XML Configuration File — PHPUnit 9.6 Manual] | |||
* [https://github.com/sebastianbergmann/phpunit/blob/42966d97ce3b66e65beb46411b4f72bf467746f2/src/Runner/ResultCache/DefaultResultCache.php#L37 phpunit/src/Runner/ResultCache/DefaultResultCache.php at 42966d97ce3b66e65beb46411b4f72bf467746f2 · sebastianbergmann/phpunit] | |||
* [https://github.com/sebastianbergmann/phpunit/blob/42966d97ce3b66e65beb46411b4f72bf467746f2/src/TextUI/Configuration/Merger.php#L86 phpunit/src/TextUI/Configuration/Merger.php at 42966d97ce3b66e65beb46411b4f72bf467746f2 · sebastianbergmann/phpunit] | |||
phpunitを実行すると、.phpunit.result.cacheファイルが作成される。 | |||
phpunit.xmlのcacheResultがデフォルトtrueになっており作成されている。 | |||
* phpunit.xml | |||
** phpunit.cacheResult: 初期値true。結果キャッシュ作成有無。 | |||
** phpunit.cacheResultFile: 初期値.phpunit.result.cache。ファイルパス。 | |||
* option | |||
** --cache-result: キャッシュ結果を出力する (既定)。 | |||
** --do-not-cache-result: キャッシュ結果を出力しない。 | |||
** --cache-result-file <file>: キャッシュ結果のパスを指定する (既定: ./.phpunit.result.cache)。 | |||
cacheResultFileは他にPHPUNIT_RESULT_CACHE環境変数でも設定できる模様。 | |||
phpstanの結果キャッシュをvar/tmp/phpstanに配置しているので、これにならってvar/tmp/phpunit/.phpunit.result.cacheにするといいかも。 | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<phpunit bootstrap="tests/bootstrap.php" cacheResultFile="var/tmp/phpunit/.phpunit.result.cache" colors="true"> | |||
<testsuites> | |||
<testsuite name="tests"> | |||
<directory>tests</directory> | |||
</testsuite> | |||
</testsuites> | |||
</phpunit> | |||
===== getallheaders() ===== | |||
* [https://stackoverflow.com/questions/41427359/phpunit-getallheaders-not-work http headers - PHPUnit - getallheaders not work - Stack Overflow] | |||
* [https://www.php.net/manual/ja/function.getallheaders.php PHP: getallheaders - Manual] | |||
Error: Call to undefined function getallheaders() | |||
apache_request_headers()のエイリアス。サーバー固有のAPIはPHPUnit実行時は使えないので、bootstrap.phpに、代替関数を用意する。 | |||
if (!function_exists('getallheaders')) { | |||
function getallheaders() { | |||
$headers = []; | |||
foreach ($_SERVER as $name => $value) { | |||
if (substr($name, 0, 5) == 'HTTP_') { | |||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; | |||
} | |||
} | |||
return $headers; | |||
} | |||
} | |||
===== PHP Fatal error: Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in ===== | |||
PHP Fatal error: Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in /common/php/src/PHPUnit/Framework/SelfDescribing.php on line 58 | |||
Fatal error: Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in /common/php/src/PHPUnit/Framework/SelfDescribing.php on line 58 | |||
while running parallel worker | |||
古いPHPUnitがある環境に、新しめのPHPUnitがある状態で、PHPStanを実行すると上記のエラーが出てしまった。 | |||
https://chatgpt.com/share/6837b3e4-01ac-800b-a547-164fcca8dbf8 | |||
古いPHPUnitと新しいPHPUnitとで、同じクラスでも指定方法が変わっている。その都合で、両方がinclude_pathにあると競合する。include_pathから除外する必要がある。 | |||
===== テストのグループ化 ===== | |||
https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83 | |||
Jestにはdescribe()内にit()を入れるような、テストメソッドのネスト構造ができた。PHPUnitにはそれがない。 | |||
@groupアノテーションがあるが、これは--groupや--exclude-groupで指定したグループに属するテストを対象にしたり、除外したりするためのもの。ネストとは概念が異なる。 | |||
ネスト構造はクラスとメソッドの親子関係での表現になる。分けたかったら、テストメソッドの命名規則などになる。 | |||
===== 親クラスBaseTestCase ===== | |||
テストクラスで、共通の処理とかしたいことがある。親クラスにまとめたい。 | |||
PHPUntはTestCaseがテスト対象じゃないことを意味するので、親クラスも最後はTestではなくてTestCaseにする。 | |||
https://grok.com/share/c2hhcmQtMw%3D%3D_0cfa311d-b290-4d5b-9183-ca7f7efb2d53 | |||
BaseTestCaseとか。 | |||
共通テストメソッドはassertXxxにする。testXxxはPHPUnitのtest対象になるので。 | |||
===== プロセス ===== | |||
https://grok.com/share/c2hhcmQtMw%3D%3D_e1586c8a-b519-44be-b04d-185e61969ae2 | |||
phpunitは基本的に同一プロセスで全テストを実行する。その都合で、あるファイルでincludeしたシンボルはグローバルに存在するので、他のファイルにも影響ある。 | |||
あるファイルAでincludeしてシンボルAAが登場して、別のファイルBでincludeするファイル内にシンボルBBがある場合、エラーになる。 | |||
対策がいくつかある。 | |||
* 名前空間を活用してシンボル衝突を防ぐ。 | |||
* phpunit --process-isolationのオプションを指定。ただし、遅くなる。 | |||
* テストスイートの分離。 | |||
根本的には、同じシンボルが複数ファイルでグローバルで登場するのがまずい。名前空間、require_once、function_existsとかでガードすべき。元ファイルが想定していないなら、--process-isolationオプションを使う。 | |||
[[Category:PHP]] | [[Category:PHP]] | ||
2025年10月2日 (木) 15:43時点における最新版
PHPDoc
About
PHPDocはPHP流のコメントのスタイル。phpDocumentorはPHPDocからドキュメントを生成するツール。PHPDocを解析して文書を作成したり利用するソフトはいろいろある。例えば、VS CodeやPHPStormなどのIDEもPHPDocを使う。
Definition of a Type
ABNF
type-expression = 1*(array-of-type-expression|array-of-type|type ["|"])
array-of-type-expression = "(" type-expression ")[]"
array-of-type = type "[]"
type = class-name|keyword
class-name = 1*CHAR
keyword = "string"|"integer"|"int"|"boolean"|"bool"|"float"
|"double"|"object"|"mixed"|"array"|"resource"|"scalar"
|"void"|"null"|"callable"|"false"|"true"|"self"
nullable
型でnullを許容する場合、PHPDocではint|nullのように|で型をunion型のように書く。
PHP 7.1以上だと?intの書き方が許容されているが、PHPDocは|だけ。null|のように先頭に持ってくるのがわかりやすい。型は長いことがあるから。
Tag
General
よく使う@param/@returnの構文。
<type-expression = 1*(array-of-type-expression|array-of-type|type ["|"])
array-of-type-expression = "(" type-expression ")[]"
array-of-type = type "[]"
type = class-name|keyword
class-name = 1*CHAR
keyword = "string"|"integer"|"int"|"boolean"|"bool"|"float"
|"double"|"object"|"mixed"|"array"|"resource"|"scalar"
|"void"|"null"|"callable"|"false"|"true"|"self"
クラス名以外は全小文字。
基本は @<directive> <Type> <name> <description> の書式。スペース区切り。
- @property: __get/__setのマジックプロパティーを使う場合にクラスの注釈部で指定する。基本は使わない。が、型定義のない親クラスの型の明示にも使える。
- @var: 変数、プロパティー、定数で使用する。一番よく使う。
/** @var int $int This is a counter. */
$int = 0;
// There should be no docblock here.
$int++;
class Foo
{
/**
* Full docblock with a summary.
*
* @var int
*/
const INDENT = 4;
/** @var string|null Short docblock, should contain a description. */
protected $description = null;
public function setDescription($description)
{
// There should be no docblock here.
$this->description = $description;
}
}
inline tag reference
inline tag reference - phpDocumentor
以下のタグはインラインでも使用可能。ツールによって若干解釈方法が異なる。
- @example
- @internal
- @inheritdoc
- @link
- @see
以下の書式で全体を波括弧で囲んで使う。
{@tag value}
inheritance
以下の継承関係、グループがある。
| Elements | Inherited tags |
|---|---|
| Any | @author, @version, @copyright |
| Classes and Interfaces | @category, @package, @subpackage |
| Methods | @param, @return, @throws |
| Properties | @var |
Methods
@return
https://chatgpt.com/c/67998298-f248-800b-a921-a47210ab8d72
関数やメソッドの説明のために関数定義の前に書くのが正しい使い方。
return文の直前に書くものではない。returnのところを説明したかったら、通常コメント。
ただ、関数の@returnでreturnが何を返すのかを書いた方がいい。
@throws
例外が発生する場合に記載する。複数の種類の例外がありえるなら、その数だけ@throwsを記載する。
注意喚起
https://chatgpt.com/c/67998298-f248-800b-a921-a47210ab8d72
注意が必要な挙動を文書化したい場合。@warnや@noticeはない。代わりの気泡を使う。
| タグ | 使い道 |
|---|---|
@todo
|
修正や機能追加が必要な場合 |
@deprecated
|
非推奨の機能を警告 |
@throws
|
例外が発生する可能性を示す |
// /* */
|
特定の処理に対する注意を記述 |
迷ったら/** */でいいと思われる。
マジックメソッド
https://chatgpt.com/c/67a5c298-bdf8-800b-9f42-8618f36274ab
独自クラスで__getや__setで独自のマジックメソッドで動的プロパティーを実装している場合。PHPDocがないと型や補完が一切されなくて辛い。
/**
* クラスの説明
*
* @property-read string $name ユーザーの名前
* @property int $age ユーザーの年齢
*/
class User {
private array $data = [];
public function __get(string $name) {
return $this->data[$name] ?? null;
}
public function __set(string $name, $value): void {
$this->data[$name] = $value;
}
}
$name/$ageのプロパティー名のプロパティーが動的に追加されるなら、@propertyで変数名と型を書いておくと補完してくれる。
https://chatgpt.com/c/67a9546f-904c-800b-9459-af827e0b9fee
__getに@returnしてもいいが、これはプロパティーの情報がない。__getに@returnするくらいなら、@property(-read) でプロパティーとセットで書いた方がいい。
Other
Array
PHPでよく使うArray。PHPDocでの表現方法がある。
- @return array
- @return int[]
- @return int[][]
- @return (int|string)[]
PHPDocでの配列の表現方法は以上。単純配列の配列の配列の場合、[]を増やせばいい。
連想配列については説明なし。arrayしかない。単純配列はPHPDocのこれで問題ない。問題は連想配列。
ArrayShape
- Array types - Documentation
- PHPDoc Types | PHPStan
- ArrayShape : The PhpStorm Blog | The JetBrains Blog
- PhpStorm の新機能 - 究極の進化を遂げた最強の PHP 開発環境
PHPDocでは未対応の連想配列だが、他の静的解析ツールの独自拡張で対応している。ArrayShapeという注釈。generics arrayとも呼んでいる。
いくつかの記法がある。
function getUserData(): array {
return [
'name' => 'John Doe',
'age' => 30,
'email' => 'john.doe@example.com',
];
}
* @return array{name: string, age: int, email: string}
/**
* @return array{
* user: array{name: string, age: int, email: string},
* address: array{city: string, zip: int}
* }
*/
@var array{foo: int, bar?: string} <var-name> 説明。
@var array<array{foo: int, bar?: string}> <var-name> 説明。
【PHP】タイプヒンティングをより強力にするArrayShape - デザインワン・ジャパン Tech Blog
こういう書式。ただし、この書式だと要素は説明できないので変数全体のところに説明を入れる。
連想配列のキーが可変の場合。以下のように<>で型だけ指定する。{}はキー名が決まっている場合。
array<string, int>
リスト型で、要素ごとに型が決まっているなら、キー部分を数字にすると型を明示できる。
* @return array{0: string, 1: int, 2: string}
/**
* @param array{
* 0: string, // ユーザー名
* 1: int, // 年齢
* 2: bool // アクティブフラグ
* } $userData
*/
function processUser(array $userData): void {
// ...
}
/**
* @param list{
* string, // ユーザー名
* int, // 年齢
* bool // アクティブフラグ
* } $userData
*/
function processUser(array $userData): void {
// ...
}
/** @var array{0: string, 1: int, 2: bool} $userData ユーザー情報:名前, 年齢, アクティブ */
3番目の形式がいい気がする。
stdClass
https://chatgpt.com/c/67b833b8-1d20-800b-bc27-16dfa133e3b0
関数で複数の値を一度に返したい時がある。その際の選択肢はオブジェクトか配列。
オブジェクトはstdClass。stdClassは動的にプロパティーを設定する前提。だが、phpdocでうまく認識してくれない。
@var stdClassでこの後ろに説明を掛けるくらい。
配列の方がまだいいか。
プロパティーの継承
https://chatgpt.com/c/67cab3d0-eac4-800b-96c3-85ea480d1d91
子クラス側で再定義してしまうと、そちらが優先される。再定義しなければ、親クラスのプロパティーのphpdocが使われる。
型を使いこなすためのPHPDocの書き方 - RAKUS Developers Blog | ラクス エンジニアブログ
例えば、外部のフレームワークを使っていて、そのベースクラス側で型定義がない場合、継承後に@propertyで使いたいプロパティーの型を明示できる。
この方法を使わない場合、同じプロパティーを初期値指定で再定義しないといけない。@propertyで指定するとそれを回避できる。
phpDocumentor
Ref: Home | phpDocumentor.
PHPのソースコードにコメントを残す際に、構文に従って記載すると、ツールで表示したり、文書に出力できたりする。ソースコードリーディングにも役立つので、積極的に記載したほうがよさそう。構文を整理しておく。
特に記法が大事。
ファイル冒頭の<?php の直後あたりに書くと、ファイルレベルのDocBlockになる。逆にclassの直前などに書くと、ファイル冒頭でもclassレベルになる。
以下の要素に前置できる。
- require(_once)
- include(_once)
- class
- interface
- trait
- function (including methods)
- property
- constant
- variables, both local and global scope.
Inheritance
DocBlockはSummary/Descriptionを上書きしたり、拡張できる。@inheritdocを使う。
要素ごとに以下のタグを継承する。
| Elements | Inherited tags |
|---|---|
| Any | @author, @version, @copyright |
| Classes and Interfaces | @category, @package, @subpackage |
| Methods | @param, @return, @throws |
| Properties | @var |
@subpackageタグは同じ@packageの親クラスのときだけ継承される。
一番よく使う変数の説明は@var (変数、プロパティー) と@param (関数引数)。
DocBlock
DocComments
DocBlockはDocCommentと呼ばれるコメントで囲まれる。DocCommentは/**で始まり、*/で終わる。そして、DocComment内の行の先頭は* で始まるべき。
<?php
/**
* This is a DocBlock.
*/
function associatedFunction()
{
}
/** This is a single line DocComment. */
複数行形式と1行形式がある。
変数などの説明には1行形式でいいと思う。
PHPDoc
DocBlockは3部構成。
- Summary=短い説明。改行直前の.か空行で終わり。
- Description=長い説明。アルゴリズムの機能や、使用方法、例など。最初のタグか、改行、DocBlockの終端で終わる。
- Tags/Anntations=要素のメタ情報。新しい行の@から始まる。
具体例。
<?php
/**
* A summary informing the user what the associated element does.
*
* A *description*, that can span multiple lines, to go _in-depth_ into
* the details of this element and to provide some background information
* or textual references.
*
* @param string $myArgument With a *description* of this argument,
* these may also span multiple lines.
*
* @return void
*/
function myFunction($myArgument)
{
}
Summary
/** * This is a summary * * This is a description */
/** * This is a summary. * This is a description */
https://chatgpt.com/c/67400d8d-0300-800b-8db3-f3453fee3355
なお、summary/descriptionとtagの間は空行はあってもなくてもいい。縦に間延びするのでなくていいと思う。
空行があると、*の直後に終端スペースが残ったりしてごみが入ることがあるし。
Usage/code example
https://chatgpt.com/c/674687f5-6fc0-800b-95a6-34758bfda434
サンプルコードの埋め込み表示はmarkdownのコードブロック記法を使う。
/**
* この関数は数値を二倍にします。
*
* 使用例:
* ```php
* $result = double(5);
* echo $result; // 出力: 10
* ```
*
* @param int $number 入力値
* @return int 倍になった値
*/
function double(int $number): int {
return $number * 2;
}
外部ファイルにサンプルファイルがある場合、@exampleタグで外部ファイルのパスを相対URIか絶対URIで指定できる (@example - phpDocumentor)。phpdocでフォーマット時におそらく、リンクなどで中身をみれるようにしてくれるのだと思う。
https://chatgpt.com/c/67998298-f248-800b-a921-a47210ab8d72
他に@see/@tutorialも使える。が、専用のものはない。/** */ 内でUsageなどで普通の文章で説明するしかない。
Package manager
Composer
PHPのパッケージ管理システム。PHP v5.3.2以上で動作する。
Install
Ref: インストール: Composer | モダンなPHPのパッケージマネージャー – senooken JP.
LOCAL=~/.local PKG=composer VER= DIR=$LOCAL/stow/$PKG-$VER/bin [ -e installer ] || wget https://getcomposer.org/installer [ -e installer ] || curl -LO https://getcomposer.org/installer mkdir -p $DIR php installer --install-dir="$DIR" --filename=$PKG
公式サイトからinstallerをダウンロードしてそれを使って任意の場所に設置する。
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php --install-dir=$HOME/.local/bin --filename=composer
これでもいい。
Usage
Composerを使う場合,composer.jsonファイルを用意する。このファイルはプロジェクトの依存関係を記載する。VCSで管理すべきファイルだ。
このファイルに使用するライブラリーを以下のように記入する。
<{
"require": {
"monolog/monolog": "1.0.*"
}
}
composer.jsonに指定する最初の項目はrequireキーだ。このキーで依存パッケージをComposerに知らせる。パッケージ名とバージョンを指定する。
新規にパッケージを追加する場合は、以下のコマンドでインストールとcomposer.jsonへの追記を行えます。同時に、composer.lockファイルも作成されます。composer.lockも管理すべきファイル。
composer require "monolog/monolog:1.0.*"
パッケージ名はベンダー名とプロジェクト名から構成される。
1.0.*は1.0の任意のバージョンを示す。
composer.jsonを用意したら,以下のようにcomposerのinstallコマンドを実行する。
php composer.phar install
composer install
これにより,vendorディレクトリーにパッケージがインストールされる。デフォルトでrequire-devの開発用パッケージもインストールする。除外したければ、--no-devのオプションを指定する。
プロジェクトにgitを使っている場合,.gitignoreにvendorディレクトリーを追加したほうがいい。
Composerによるインストールが完了すると,composer.lockファイルにダウンロードしたパッケージとバージョンを出力する。composer.lockをプロジェクトリポジトリーに追加して,プロジェクトメンバー全員が同じバージョンのパッケージを使用する。
composer.lockが存在するプロジェクトで上記コマンドを実行する場合,composer.jsonの内容に加えて,composer.lockの内容も参照されて,composer.lockと同じバージョンがインストールされる。
パッケージを最新バージョンに更新したい場合,composer updateコマンドを使う。このコマンドを実行すると,最新バージョンをインストールして,composer.lockも更新する。動作としては,composer.lockを削除後にcomposer installを実行することと等しい。composer updateは基本的には使わない。
updateやinstallの後にパッケージ名を指定すると,指定したパッケージだけ更新やインストールできる。
composer update monolog/monolog
Composerでインストール可能な主なパッケージは,Packagistリポジトリーに配置されている。
Autoloading (自動読み込み )
About
Basic usage - Composer
ライブラリーの自動読み込みのために,Composerはvendor/autoload.phpファイルを生成する。以下のように,ライブラリーのクラスを使用するファイルの先頭でこのファイルを読み込めば使えるようになる。
<php
require __DIR__ . '/vendor/autoload.php';
$log = new Monolog\Logger('name');
$log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING));
$log->addWarning('Foo');
このほかに、composer.jsonのautoload欄に指定することで,ライブラリー以外の自分のコードも自動読み込みの対象にできる。
{
"autoload": {
"psr-4": {"Acme\\": "src/"}
}
}
PSR-4
PSR-4: Autoloader - PHP-FIG psr-4はPHPの標準仕様。 名前空間、クラスメイト、ファイルパスとの対応関係を記している。
- 名前空間の単位ごとに、ディレクトリーに対応。
- 大文字小文字は区別。
- 大文字小文字を区別して、ファイル名は.phpの拡張子で終わる。
dump-autoload
composer.jsonを編集した場合、composer dump-autoloadを実行してvendor/autolaod.phpを必ず更新します。
https://chatgpt.com/share/6837bdb1-1798-800b-a982-650ce38bc30a
なお、require_onceではなくrequireになっているのには理由がある。
- そもそも何回も読み込む用途ではない。普通アプリのルートで1回しか読み込まない。
- 万が一複数回読み込んでも、内部的にspl_autoload_registerを呼んでいて、重複登録されない。
- requireのほうが読み込み確認がないぶんわずかに早い。
Libraries
自前のライブラリーをComposerでインストール可能な形式にする方法がある。
Every project is a package
ディレクトリーにcomposer.jsonがあると、そのディレクトリーはパッケージになる。プロジェクトとパッケージの違いは、名前の有無。プロジェクトは名前のないパッケージという扱いになる。
パッケージをインストール可能にするにあたって、composer.jsonに最低限名前が必要。
{
"name": "acme/hello-world",
"require": {
"monolog/monolog": "1.0.*"
}
}
acme/hello-worldというプロジェクトになる。acmeはベンダー名で、ベンダー名は必須。
ベンダー名に迷う場合、GitHubのユーザー名が適している。パッケージ名は小文字必須。単語区切りは-にするのが慣例。
Library Versioning
VCSでパッケージを管理している場合、composerはVCSからバージョンを自動で判別する。VCSを使っていない場合だけ、versionプロパティーを追加する。
{
"version": "1.0.0"
}
Publishing to a VCS
composer.jsonを用意したらVCSのリモートリポジトリーに公開する。ベンダー名とユーザー名は不一致でも問題ない。
公開したパッケージを取り込む場合、requireで指定する。
{
"name": "acme/blog",
"repositories": [
{
"type": "vcs",
"url": "https://github.com/username/hello-world"
}
],
"require": {
"acme/hello-world": "dev-master"
}
}
パッケージ名hello-worldに必要なリポジトリーの情報をrepositoriesで指定している。たぶん、末尾のパッケージ名とリポジトリー名は一致が必要。
Publishing to packagist
VCSでの公開のケースは以上。ただ、repositoriesの情報は省略する方法がある。これは、Packagistに登録している場合。composerはpackagitstから同盟パッケージを探す。公開して問題ないなら、Packagistへの登録を検討する。
Light-weight distribution packages
.githubディレクトリーのように、パッケージに不要なファイルがある。
.gitattributesでパッケージやzipに含めないファイルを指定できる。
// .gitattributes /demo export-ignore phpunit.xml.dist export-ignore /.github/ export-ignore
以下のコマンドで確認できる。
git archive branchName --format zip -o file.zip
パッケージに含まれないだけで、Gitリポジトリーには入っている。
Distribution
php - How to include Composer within a GitHub repository - Stack Overflow
Composerを使ったプロジェクトを一般配布する場合。ユーザーにcomposer installしてもらえるならリポジトリーのまま配布すればいい。
composer installしてもらえないならば、以下の2の方法をとるしかない。
- vendorディレクトリーをコミットに含める。
- リリース用zipにのみvendorを含める (例: Yoast/wordpress-seo: Yoast SEO for WordPress)。
Yoast SEOはGruntでリリース用に用意している模様。
composer.jsonにscriptsを含めることできるし、package.jsonでもOK。
Repositories
パッケージとリポジトリーの概念。
Concepts
Package
パッケージは何かを含むディレクトリー。名前とバージョンを含んでいて、パッケージを識別する。
Repository
パッケージのソース。パッケージとバージョンのリスト。
Types
リポジトリーの種類を指定する。
- composer: デフォルト
- vcs: gitリポジトリー。ローカルのgitリポジトリーもurlにパス指定で対応できるが、基本はURL。ローカルはtype=path指定で対応する。
- package: zipファイルなど。
Hosting your own
外部のホスティングサービス、サイトなどに配置していないものをリポジトリーとして使いたいことがあったりする。
例えば、社内のライブラリーのビルド結果とか。プライベートなリポジトリーとか。
- type=artifact: ZIPやtarファイル。ルートにcomposer.jsonを含む。
- type: path: 絶対パス、相対パスでローカルディレクトリーを指定。VCSの場合、type=vcsでurlを相対パスでもできるが、ローカルの場合はpathにするのがよさそう。。バージョンは現在のブランチ・タグから推測する。あるいは、パッケージのcomposer.jsonで明示。どれでも解決できない場合、dev-masterとみなす。
Articles
Scripts
composerの実行中に生じるイベントに対して行うコールバックがScriptsの基本的な概念。それとは別に、独自コマンドも追加できる。
testなどちょっとした定型コマンドの実行に便利。
Event names
いくつかイベントがある。
- Command Events
- post-install-cmd: install実行後。
Writing custom commands
composer.jsonの以下のプロパティー定義でcomposer testでphpunitを実行できる。
{
"scripts": {
"test": "phpunit",
"do-something": "MyVendor\\MyClass::doSomething"
"my-cmd": "MyVendor\\MyCommand"
}
}
コマンドにオプションを渡す場合、--で区切る。
composer test -- --filter <pattern>
これでtestに--filter <pattern>を渡す。
コマンドの他に、PHPコード自体も実行できる。
composer do-something arg
これでstatic function doSomething(\Composer\Script\Event $event)が呼ばれる。
Executing PHP scripts
@phpや@composerと@を前置すると、composer.jsonを呼び出している@php/@composerを流用してコマンドを実行できる。
{
"scripts": {
"test": [
"@php script.php",
"phpunit"
]
}
}
また、通常のシェルスクリプトを記述する場合、PHP_BINARYの環境変数で、実行中PHPのフルパスを参照できる。
Versions and constraints
Versions and constraints - Composer
composer requireなどで指定するパッケージのバージョンにはいくつか記法がある。このバージョン部分は、composerではversion constraint (バージョン制約) と呼んでいる。このバージョン制約で、チェックアウト対象を判断する。
~/my-library$ git branch v1 v2 my-feature another-feature
~/my-library$ git tag v1.0 v1.0.1 v1.0.2 v1.1-BETA v1.1-RC1 v1.1-RC2 v1.1 v1.1.1 v2.0-BETA v2.0-RC1 v2.0 v2.0.1 v2.0.2
tag
基本的に、composerはタグを扱う。
上記のようなタグの場合、composerは先頭のvなどのプレフィクスを除外して考える。基本的にはこの中で、一番新しいものを優先的に探す。
branch
タグではなく、ブランチのチェックアウトが必要なら、特別なdev-*プレフィクス/サフィックスを指定してブランチを指定する。
上記の例で、my-featureブランチの指定が必要ならば、dev-my-featureを指定する。
ブランチ名がバージョン名と似ている場合、記法が変わる。v1.x-devのように指定する。v1タグではなく、v1ブランチを明示するために、.xは必須。あるいは、タグ名とブランチ名を完全に別の名前 (v1ブランチの代わりにv1.xブランチ) にしておけば、.xは不要。
バージョン名によく似たブランチ名を指定する場合だけ、dev-プレイフィクスではなく、-devサフィックスを指定する。
Test
General
Static
- 静的コード解析 - Wikipedia
- PHPerのための「静的解析」を語り合う【PHP TechCafe イベントレポート】 - RAKUS Developers Blog | ラクス エンジニアブログ
- PHPコードの静的解析ツールたち - Innovator Japan Engineers’ Blog
- 行事: 「12月にPHP8.3が出るので、PHP8で増えた文法をおさらいしましょうセミナー」参加報告 | PHP8対応の肝は型とエラーレベル | GNU social JP Web
PHPの静的解析ツール。
- php -l
- PHPStan
- Larastan: LaravelでPHPStanを使うと、static呼び出しなどで大量のエラーが出ますので、それらをカバーしてくれます。
- PHP Code Sniffer
- PHPMD
- Psalm
- PhpStorm
- Rector
上記が有名。
PHPStanとPsalmはベースラインを設定できるので既存のプロジェクトにも導入しやすい。
PHPのバージョンアップの互換性の確認などもできる。
古いスタイルのPHPの改善
https://grok.com/share/c2hhcmQtMw%3D%3D_938039fe-7a79-4b4b-aad5-6b0fb3e71c8f
https://grok.com/share/c2hhcmQtMw%3D%3D_87563ab0-56ca-4faa-aa15-f5fb31dcf52d
昔ながらの、要請が来たらheader/echoで手動で応答を返すスタイルのPHPコード。このままだと、テストとかしにくいし、同一コードが散見して冗長になる。保守に問題がある。
改善手順がある。
- 静的解析ツール (php -l/phpstan)
- 仕様化テスト (phpunit)
- リファクタリング (関数抽出)
- リファクタリング (クラス化)
- 単体テスト
仕様化テスト/Characterization test
読書メモ『レガシーコード改善ガイド』マイケル・C・フェザーズ|まくろぐ
特徴付けテストと直訳することもあるらしい。マイケル・C・フェザーズの2009年翻訳のレガシーコード改善ガイドで提唱された内容。著者の独自の概念。
ドキュメントや仕様が不明瞭なコードを扱う際に有効。既存コードの動作を把握し、そのふるまいを固定化するための手法。
現状コードが今何をするのかをテストで記録する。リファクタリングとかをする前に現在の動作をある程度保証するテストを記載することで、意図しない影響を防げる。
例えば、昔ながらの、phpファイルがそのまま応答を返すタイプのアプリの場合。
環境変数を設定して、バッファリングで、文字列の有無で、ある程度現在のふるまいをテストできる。
PHPUnitでやる場合。
<?php declare(strict_types=1);
namespace Tests\Unit\Service\Resign;
final class SsResignConfirmTest extends \Tests\Unit\BaseTestCase
{
public function testBaseline(): void
{
$this->assertBaseline('service/resign/ss_resign_confirm.php');
}
}
<?php declare(strict_types=1);
namespace Tests\Unit;
/**
* 単体テストのベースクラス。
*
* tests/Unit/以下はこのクラスを継承して実装する。基本は、継承先の子クラスのtestBaselineでassertBaselineを呼び出す。
*
* 他に、テストで共通で行うようなものがあれば、こちらに記述して共通化していく。
*/
abstract class BaseTestCase extends \PHPUnit\Framework\TestCase
{
/**
* 仕様化テスト (characterization test) として、該当ファイルの応答にエラーがないことを確認する共通テストメソッド。
*
* 継承先の子クラスのtestBaseline内で `$this->assertBaseline('path');` で実行想定。
* @param string $relative_include_path テスト対象のファイルの相対パス。
*/
protected function assertBaseline(string $relative_include_path): void
{
ob_start();
include PROJECT_ROOT . '/' . $relative_include_path;
$html = ob_get_clean();
$this->assertStringNotContainsString('URLが不正です。', $html);
/** err.tpl */
$this->assertStringNotContainsString('<title>エラー', $html);
}
こんな感じで、output bufferingでechoのHTTP本体相当を変数に格納して、その変数にエラー文字がないかでチェックする。
https://grok.com/share/c2hhcmQtMw%3D%3D_d3073260-4256-46a8-8762-ef7c63967681
仕様化テストだから、Baseじゃなくて、Baselineとするのがいい。assertBaseline/testBaselineでやると、一貫性ある命名規則にできる。
https://grok.com/share/c2hhcmQtMw%3D%3D_57517641-d440-468e-ae2e-956958ce27f9
API系の場合、結合テストに近い。サーバーを立てて、リクエストを送って、レスポンスがどうなるかをテストする感じ。
一部クラスになっている場合も、基本は現在の挙動確認。現在の挙動を確認するような動作のテストをする。最終出力回りを重点的に書くとよい。
関数抽出
<?php
require 'smarty_setup.php'; // Smarty初期化
header('Content-Type: text/html; charset=UTF-8');
$id = $_GET['id'];
$db = new PDO('...'); // DB接続
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch();
$smarty->assign('user', $user);
echo $smarty->fetch('user.tpl');
?>
<?php
function fetchUser($db, $id) { // ロジック関数
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch();
}
function renderUser(Smarty $smarty, $user) { // ビュー関数
$smarty->assign('user', $user);
return $smarty->fetch('user.tpl'); // echoせず返す
}
// メインスクリプト
require 'smarty_setup.php';
header('Content-Type: text/html; charset=UTF-8');
$db = new PDO('...');
$id = $_GET['id'];
$user = fetchUser($db, $id);
$output = renderUser($smarty, $user);
echo $output;
?>
こんな感じで、内容ごとに処理を関数にまとめる。
- header出力
- DB操作
- テンプレート操作
関数化した部分のテストは、仕様化テストと同様で、phpunitでoutput bufferingで出力部分を破棄して、関数定義だけ取り込んでテストする。
長期保守前提であれば、関数化の段階をすっ飛ばして、オブジェクト・クラス化したほうが手っ取り早い。
クラス化
https://grok.com/share/c2hhcmQtMw%3D%3D_21361425-16d6-419a-84ae-3c793d627eef
シンプルで無難なMVCの形でクラス化する。
- DB操作: Modelクラス
- 画面表示: Viewクラス
- ロジック呼び出し: Controllerkクラス
コード例: Model (models/UserModel.php):
class UserModel {
private $db;
public function __construct(PDO $db) {
$this->db = $db;
}
public function fetchUser($id) {
$stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch();
}
}
View (views/UserView.php):
class UserView {
private $smarty;
public function __construct(Smarty $smarty) {
$this->smarty = $smarty;
}
public function renderUser($user) {
$this->smarty->assign('user', $user);
return $this->smarty->fetch('user.tpl');
}
}
Controller (controllers/UserController.php):
class UserController {
private $model;
private $view;
public function __construct(UserModel $model, UserView $view) {
$this->model = $model;
$this->view = $view;
}
public function handleRequest() {
$id = $_GET['id'];
$user = $this->model->fetchUser($id);
return $this->view->renderUser($user);
}
}
メインスクリプト (user.php):
<?php
require 'autoload.php'; // Composerのautoload
$db = new PDO('...');
$smarty = new Smarty(); // Smartyセットアップ
$model = new UserModel($db);
$view = new UserView($smarty);
$controller = new UserController($model, $view);
header('Content-Type: text/html; charset=UTF-8');
$output = $controller->handleRequest();
echo $output;
?>
これでMVCパターンに近づく。SmartyをViewクラスでラップしてテストしやすく。
あるいは、メインスクリプトをControllerとみなして、constructorで画面表示処理を書くというのもありかもしれない。
if (!debug_backtrace()) {}
このコードでincludeとの違いも検知できる。
PDOなどのDB接続インスタンスは、メインスクリプトで従来通り毎回書くか、シングルトンかstaticメソッドで管理して渡すとか。
.inc/.include/.class
https://grok.com/share/c2hhcmQtMw%3D%3D_6193ad5e-6df3-42c7-9dbc-dc67deb44bcd
phpファイルが直接応答を返す古いパターンだと、拡張子が.inc/.include/.classなど.phpじゃないことがある。
公開ディレクトリーに、同居する都合、サーバーアクセスで拡張子で、アクセス制御しているからだろう。
本来であれば、現代的なpublic/index.phpでコントローラーを明示的に振り分けて、ユーザーがアクセス可能なディレクトリーを制限すべきだろう。
PHP
declare(strict_types=1);
新規ファイルには、基本的に指定したほうがいい。既存ファイルは慎重に。
php -l
-l/--syntac-checkオプションで構文チェックのみを行う。-lはlintのlだと思われる。
成功したらNo syntax errors detected in <filename> が標準出力に書き込まれ、リターンコードは 0
失敗した場合、テキスト Errors parsing <filename> に加え、内部パーサエラーメッセージ が標準出力に書き込まれ、シェルリターンコードは、 -1 となります。
このオプションは、(未定義の関数のような)致命的なエラー(fatal error) はみつけません。致命的なエラーについても調べたい場合は、 -f を使用してください。
-lは-r (コードの実行)とは併用不能。以下のコードで全phpファイルをチェックできる。
for i in `find . -name \*.php`; do php -l $i | grep -v "No syntax errors"; done
上記コードより、以下の方が少し早い。
find . -name \*.php -exec php -l {} \; | grep -v '^No syntax errors'
外部ツールも不要で手軽で悪くない。特に、-lで実行するphpのバージョンを変えるだけでチェックでき、PHPのバージョンアップ時の事前の簡易チェック、粗探しとしてとして悪くない (PHPerのための「静的解析」を語り合う【PHP TechCafe イベントレポート】 - RAKUS Developers Blog | ラクス エンジニアブログ)。
ただ、php -lは構文エラーのみで、型チェックはできない。PHPStanを使うしかない模様。
gitのpre-commitに登録する場合、以下のような内容にするとよい。
#!/bin/sh ## Lint added/modified PHP file. set -eu has_error=false PHP_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.php$' || :) [ -z "$PHP_FILES" ] && exit || : while read -r file; do php -l "$file" | grep -v '^No syntax errors' && has_error=true done <<-EOT $PHP_FILES EOT $has_error && exit 1 || : ./vendor/bin/phpstan analyze --memory-limit 5G $PHP_FILES || has_error=true $has_error && exit 1 || : # parameters.pathsのコメントアウトを解除して全体解析する場合、上記のファイル単位のphpstanをやめて、以下の全体解析を実行する。 # ./vendor/bin/phpstan analyze --memory-limit 5G || exit 1
ただ、根本的なPHPの構文エラーがあると、PHPStanはそこで失敗して詳細情報がない。php -lもあっていい気がする。
PHPStan
導入が簡単なので黙って導入したらよさそう。
User Guide
Getting Started
composer require --dev phpstan/phpstan
以下のコマンドでバージョンを確認できればOK。
vendor/bin/phpstan analyze --version
PHPStan - PHP Static Analysis Tool 2.1.10
git pre-commit
hooks/pre-commit
#!/bin/sh ## Lint added/modified PHP file. set -eu has_error=false PHP_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.php$' || :) [ -z "$PHP_FILES" ] && exit || : while read -r file; do php -l "$file" | grep -v '^No syntax errors' && has_error=true done <<-EOT $PHP_FILES EOT $has_error && exit 1 || : ./vendor/bin/phpstan analyze --memory-limit 5G $PHP_FILES || has_error=true $has_error && exit 1 || : # parameters.pathsのコメントアウトを解除して全体解析する場合、上記のファイル単位のphpstanをやめて、以下の全体解析を実行する。 # ./vendor/bin/phpstan analyze --memory-limit 5G || exit 1
# phpstan.dist.neon
includes:
- phpstan-baseline.neon
parameters:
level: 0
tmpDir: var/tmp/phpstan
# parallel:
# processTimeout: 15000.0
# scanDirectories:
# - ../../
# todo ある程度指摘対応ができたら常に全体解析にする。
# paths:
excludePaths:
- var
最初は修正ファイルだけphpstanで解析して、安定してきたらプロジェクト全体を解析する形にするとよいだろう。
phpstanのファイル単独実行は時間がかかるので、修正対象をまとめて実行した方がいい。
https://grok.com/share/c2hhcmQtMw%3D%3D_85f34631-62c7-426a-89cb-ecb407c0246b
なお、WSLやDockerを使っている場合、docker exec <container-name> php -lなどで、docker内のphp/phpstanをホスト側で使うのがいい。
docker runで起動している場合、--nameでコンテナー名を固定する。
Command Line Usage
Analyzing code
vendor/bin/phpstan analyse [options] [<paths>...]
いくつか重要なオプションがある。
- paths: 検査対象ファイルパス。設定ファイルで指定可能。
- --level|-l: 実行レベル。設定ファイルで指定可能。
- --configuration|-c: 設定ファイルを指定する。
- --generate-baseline|-b: ベースラインを作成する。オプション引数で出力ファイルのパスを指定できる。デフォルトはphpstan-baseline.neon。
- --memory-limit: php.iniと同じ形式で最大メモリーを指定。
Running without arguments
PHPStanは基本的に、コマンド引数で指定した、ディレクトリー類を対象に解析する。
毎回コマンド引数を指定するのは手間なので、設定ファイルに記述しておくこともできる。
以下の条件を満たせば、引数なしで、設定ファイルの内容で解析できる。
- phpstan.neonかphpstan.neon.distの存在。
- pathsに解析対象パスリストが存在。
- levelパラメーターの存在。
最小限の例。
parameters: level: 0 paths: - src - tests
現実的な例。
includes:
- phpstan-baseline.neon
parameters:
level: 0
parallel:
processTimeout: 15000.0
tmpDir: var/tmp/phpstan
scanDirectories:
paths:
excludePaths:
Minimum file
https://chatgpt.com/c/67fe029c-9ce0-800b-9397-8e1d7f18b303
phpstanは基本的に引数で<paths>でディレクトリーやファイルを指定する。ただし、ここで指定していないファイルは、見に行けないので未定義の警告などが出る。
基本はパス全体を指定する。コマンドライン引数か、phpstan.dist.neonで指定しておく。
他に、pre-commitように、autoload_filesかbootstarpFilesでベースの依存ファイルを指定する。
他に、phpstanはcomposerのautoloadを理解するので、composer.jsonにautoloadを追加する。
Reflection error: Circular reference to class
名前通り循環参照が起きている。具体的に、自分と同名のクラスを最終的にextendsしている。
元ファイルを直す他、excludePathsで検査対象などから除外すれば回避できる。
Internal error: Child process timed out after 3000.0 seconds. Try making it longer with parallel.processTimeout setting.
https://chatgpt.com/share/68020423-6350-800b-9200-3d8c2d2483e2
--generate-baselineを指定すると、初回だけ時間がかかる。
parallel.processTimeoutでタイムアウトの時間を設定する。成功させるために、進捗率と現在の設定の比率でカバーできるだけの時間にする。3000で40 %くらいの進捗だったら9000とか。
初回だけ時間がかかる。
Syntax error, unexpected T_EXTENDS on line 18
https://chatgpt.com/c/6801fda7-2434-800b-84da-601c44645488
phpstanを実行すると上記のような、エラーが出ることがある。これは、PHPStanの前に根本的なPHPの構文エラーの可能性が高い。
PHPStan実行の前に、先にphp -lで解決しておく必要がある。
time -p (find . -name \*.php -exec php -l {} \; | grep -v 'No syntax errors' >php-l.log) 2>&1
Error while loading phpstan-baseline.neon: Invalid UTF-8 sequence.
作成したBaselineをincludeで読み込んで実行すると上記のエラーが出た。
https://chatgpt.com/share/68020423-6350-800b-9200-3d8c2d2483e2
ベースラインファイルにへんな文字が入ったのが原因。
以下のコマンドで変な文字の行を特定できる。
grep -axv '.*' phpstan-baseline.neon
--error-format=rawを指定してbaselineを作り直すと良い。
The Baseline
PHPStanはレベルベースでチェックしてくれる。が、既存コードで警告が多い場合、新規追加分だけ考慮したいだろう。
そういう場合に、ベースラインが役立つ。
以下のように、--generate-baselineオプションを指定して、PHPStanを実行すると、現在のエラーリストをエクスポートしてくれる。
vendor/bin/phpstan analyse --generate-baseline
ベースラインファイルを使う場合、オプションで指定するか、設定ファイルで明記する。
デフォルトでphpstan-baseline.neon。オプション引数で、出力ベースラインファイルを指定できる。
作成したら、設定ファイルphpstan.dist.neonに読み込む。
includes: - phpstan-baseline.neon parameters: # your usual configuration options
これで次回からはベースラインのエラーを無視してくれる。必要に応じて、ベースラインを編集したらい、ベースラインを再生成してもいい。
なお、ベースラインで無視したエラーがなくなった場合、ベースラインファイルから削除するまで、PHPStanは通知する。この通知を無効にしたかったら、以下を設定する。
parameters: reportUnmatchedIgnoredErrors: false
なお、ベースライン作成時に、以下の警告が出ることがあり、全ての指摘をベースラインに出力できるわけではない。
[WARNING] Baseline generated with 79012 errors. Some errors could not be put into baseline. Re-run PHPStan with "-vv" and fix them.
致命的なエラーはベースラインに出力できないので、直すしかない。
Discovering Symbols
https://chatgpt.com/c/67e0b340-da84-800b-89c8-4c72691fdc44
pathsで指定したファイルで使用されているシンボルをPHPStanは必要とする。デフォルトで以下の2箇所を探す。
- pathsの対象 (コマンドライン引数と設定ファイル)。
- composerの依存関係
基本はこれで見つかるはず。
Third party code outside of Composer dependencies
PEARとか追加で設定が必要だったりする。そういう場合、scanFilesとscanDirectoriesの設定が使える。
これらで指定したファイル、ディレクトリーをシンボルとして探す。探すだけで解析はしない。
https://chatgpt.com/share/680b240a-bf28-800b-bfc2-a84f639fe0c0
プロジェクト外部はみれないらしい。
autoload_filesでautoload.phpを指定するか。bootstrapFilesで読み込む。
https://grok.com/share/c2hhcmQtMw%3D%3D_fc03d339-2ee1-4f76-85bd-ee567a2575ef
例えば、PEARの依存関係を追加したい場合、以下のコマンドでpearのパスを確認する。
pear config-get php_dir
これをscanDirectoriesに指定すればOK。
parameters:
scanFiles:
- /usr/local/php
Bootstrap
https://grok.com/share/c2hhcmQtMw%3D%3D_fc03d339-2ee1-4f76-85bd-ee567a2575ef
PHPStanの実行前に、PHPのランタイム設定したい場合、bootstrapFilesで自前の起動時ファイルを指定できる。
parameters: bootstrapFiles: - phpstan-bootstrap.php
例えば、環境変数の設定とかをこれでうまくできる。独自のautoload的なことやっている場合もこれでいける。
Output Format
--error-formatのオプションでPHPStanの報告形式を指定できる。
.neonファイルに以下の形式で指定可能。
parameters: errorFormat: json
特に重要なものがある。
- table: デフォルト。
- raw: 1行1データで機械処理用。文字化けなどが起こりにくい。
- json: IDEなどとの連携用。
- checkstyle: XML形式。CIツール連携用。
困ったらrawを指定しておく。
Ignoring Errors
Excluding whole files
Broken symbolic link
シンボリックリンクのリンク切れなどで以下のエラーが出ることがある。
Could not read file:
https://chatgpt.com/c/68088168-ac54-800b-b78e-e28743b81011
excludePathsはファイルの中身に有効で、scanDirectiriesで指定したディレクトリーに、破損したシンボリックリンクがあると、スキャン対象に入った時点で失敗する。
事前に破損したシンボリックリンクを削除しておく必要がある。
以下のコマンドで破損リンクを削除できる。
find path/to/scan -type l ! -exec test -e {} \; -exec rm {} \;
# find path/to/scan -type l ! -exec test -e {} \; -print # 確認用
Result Cache
PHPStanは分析実行結果をキャッシュする。条件は、解析対象ファイルリスト。同一じゃないとキャッシュは使えない。
キャッシュが使えると、数秒で解析が終わることもある。
tmpDir
実行結果は%tmpDir%/resultCache.phpになる。
sys_get_temp_dir() . '/phpstan' (usually /tmp/phpstan)
parameters.tmpDirで上書き指定可能。
Macだと以下のような場所。
php -r 'echo sys_get_temp_dir() ."\n";'
/var/folders/hr/9q7pmn9x5_788w3fsp9dd71r316wyt/T
このtmpDirのデフォルトは、基本的にOSを再起動すると消去される。なので、基本的にneonファイルに以下のような指定がほぼ必須。
parameters:tmpDir: var/tmp/phpstan
.gitignoreにもついでに以下を追加する。
/var/
scannedFiles
phpstanを実行すると以下のようにキャッシュの不一致が示されることがある。
time -p vendor/bin/phpstan analyze --memory-limit=5G --generate-baseline --error-format=raw -vv 2>&1 | tee phpstan.log Note: Using configuration file phpstan.dist.neon. Result cache not used because the metadata do not match: projectConfig, scannedFiles
設定ファイルと、スキャンファイル一覧のどちらか。
原因把握のために、scannedFilesを確認したい。
resultCache.phpのscannedFilesの連想配列のキーに、ファイル一覧が入っている。
確認すると、.phpの拡張子のファイルが入っている。.gitとかは含んでいない。が、tmpDirに指定したキャッシュ内の.phpは見ている。除外必要。
parameters.fileExtensionsで追加可能 (Config Reference | PHPStan)。
paths/excludePath/scanDirectories
結果キャッシュに影響のある中で、scannedFilesに影響のあるこれらの設定の関係がわかりにくいので整理する。
- pathsの指定があれば含む。
- scanDirectoriesがあれば含む
- excludePath (.analyseAndScan) の指定があれば、除外される。paths内で一部を除外する場合に使う。シンボルとしては見たい場合はexcludePath.analyseの指定。analyseと併用・別々に記載したい場合はanalayseAndScan。scanDirectories以下も同様。気持ち悪いが、analyzeではなくてanalyseじゃないと認識されない。
parameters:
paths:
- src
excludePaths:
analyse:
- src/thirdparty
analyseAndScan:
- src/broken
srcの中のsrc/thirdpartyを、解析対象に含めたくないが、シンボルとしては認識したい場合。src/brokenは見に行くとエラーになるのでシンブルとしても無視。
Config Reference
基本はphpstan.neon.distまたはphpstan.dist.neonをバージョン管理する。ユーザーはphpstan.neonで上書きできるようにする。
phpstan.dist.neonのほうが、拡張子が明示されてよく感じる。
PHPUnit
PHPUnitを使用すれば自動単体テスト (Unit test) が可能だ。
About
Version
情報源: Supported Versions of PHPUnit – The PHP Testing Framework。
PHPUnitのバージョンごとに対応しているPHPのバージョンが決まっている。
PHP v7.4に対応してい最後のバージョンはPHPUnit 9なので、当分はこれを使うのが良い。
Install
以下のコマンドでPHP 7.4に対応している最後のPHPUnitのv9をインストール。
composer require --dev phpunit/phpunit ^9
composer.jsonに以下が追加される。
{
"require-dev": {
"phpunit/phpunit": "^9"
}
}
composer.jsonとcomposer.lockをVCSで管理する。
composerのautoloadのclassmapでソースファイルのルートディレクトリーを指定する。
{
"autoload": {
"classmap": [
"src/"
]
},
"require-dev": {
"phpunit/phpunit": "^9"
}
}
最後に以下のコマンドでvendor/autoload.phpを更新。
composer dump-autoload
Getting Started with PHPUnit 9
Getting Started with Version 9 of PHPUnit
- phpunitをインストール
- テストコード記述
- phpunit実行
上記の3ステップで使う。
以下のように引数に、ディレクトリーかテスト対象ファイルの相対パスを探す。
phpunit tests
2. Writing Tests for PHPUnit
出典: 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual。
基本的な使用方法を整理する。
- 基本的にはクラス単位で試験コードを記載。<Class>クラスの試験コードは<Class>Testの命名にする。
- <Class>Test はPHPUnit\Framwork\TestCaseを継承させる。
- 試験はpublicのtest*メソッドの命名にする。あるいは、@testのアノテーションを付ければ、命名規則に従わなくてもいい。
- test*メソッド内で、assertSame() などで、期待値との比較で試験を行う。
- test*メソッドで1メソッドに対して、試験する。例えば、正常形、異常系、境界値など。1メソッド1テストメソッドにするとわかりやすい。
例:
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StackTest extends TestCase
{
private static $dbh;
private $instance;
public static function setUpBeforeClass(): void
{
// DB接続などクラス全体の初期化処理
self::$dbh = new PDO('');
}
public static function tearDownAfterClass(): void
{
self::$dbh = null;
}
protected function setUp(): void
{
// 該当インスタンスの生成などメソッド単位の初期化処理。
$instance = new Stack();
}
public function testPushAndPop(): void
{
$stack = [];
$this->assertSame(0, count($stack));
array_push($stack, 'foo');
$this->assertSame('foo', $stack[count($stack)-1]);
$this->assertSame(1, count($stack));
$this->assertSame('foo', array_pop($stack));
$this->assertSame(0, count($stack));
}
}
命名規則があっていないと、以下のメッセージが出てphpunitの実行に失敗する。
Class <Class>Test could not be found in /path/to/<Class>Test.php
後述するが、メソッドテスト時は、data providerでテストデータを与えて、1テストメソッドで1試験対象メソッドをテストするといい。
Depends
前回の試験で準備した結果を利用したい場合、@dependsのアノテーションでテスト関数を指定しておくと、指定したテスト関数のreturnを引数に受け継いだテスト関数を記述できる。
@dependsは複数指定でき、指定順に事前に試験が実行されて引数に渡される。
Data Provider
About
- 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual
- https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83
ある関数に対して、テストケースを用意して、複数の引数の組み合わせを試験したいことがよくある。こういうときのために、テスト関数に渡す引数を生成する関数のデータプロバイダーを指定できる。@dataProviderで関数を指定する。データプロバイダーは引数のリストを配列で返すようにする。
データプロバイダーを使うことで、テスト用のメソッドは1個で、データだけ変えてテストできる。1メソッド1テストメソッドで対応できてスマート。また、データプロバイダーを使うと、どのデータで失敗したかがわかる。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DataTest extends TestCase
{
/**
* @dataProvider additionProvider
*/
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}
public function additionProvider(): array
{
return [
[0, 0, 0],
[0, 1, 1],
[1, 0, 1],
[1, 1, 3]
];
}
}
data providerの使用方法。
- テストデータを配列かIteratorで返却するpublic メソッド (data provider) を用意
- テストメソッドの引数で、data providerの配列を受け付ける。
- data providerを使用したいテストメソッドで、@dataProvider <data provider method> のアノテーションを指定する。
データセットはリストでもいいが、連想配列でキーにテスト名を書くとわかりやすい。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DataTest extends TestCase
{
/**
* @dataProvider additionProvider
*/
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}
public function additionProvider(): array
{
return [
'adding zeros' => [0, 0, 0],
'zero plus one' => [0, 1, 1],
'one plus zero' => [1, 0, 1],
'one plus one' => [1, 1, 3]
];
}
}
テストデータに意味がある場合は、記載した方がよさそう。このテストデータ部分にMock用のデータを渡したりもできる。
Mockでデータに応じて処理を変える場合も、データプロバイダーのデータでif文などの条件を入れたり、mock_dataのような専用のキーを渡す形にすればいい。
データ数が多い場合、名前付き配列にしておくと、どういうデータ項目で失敗したかがわかりやすい。Iteratorオブジェクトを返してもいい。
Exception
テストデータによって、例外が発生することを期待する場合、いくつか方法がある。テストデータにthrowsException:boolの列を用意して、それで判定する。または、例外用のテストメソッドを用意してそちらにするというのもある。複雑なら後者、テストケースがそんなに多くなくてシンプルなら前者でいいと思う。
/**
* @dataProvider provideCases
*/
public function testSomething($input, $expected, $throwsException)
{
if ($throwsException) {
$this->expectException(\InvalidArgumentException::class);
}
$result = $this->target->doSomething($input);
if (!$throwsException) {
$this->assertSame($expected, $result);
}
}
public static function provideCases(): array
{
return [
'正常系' => ['input' => 10, 'expected' => 100, 'throwsException' => false],
'異常系' => ['input' => -1, 'expected' => null, 'throwsException' => true],
];
}
Naming
data providerメソッドの命名。「複数メソッド共通: provideXxx」にする。
「特定メソッド専用: testXxxDataProvider」にすると、testメソッドと対応が同じでわかりやすいのだが、testから始まると、testメソッド扱いされるので、assertがないと警告が出る。命名規則的にはxxxxTest/xxxProviderがきれいなんだけど、test/provideで使う場所と隣同士にすれば、そんなに困ることはないだろう。
他に、返却する配列の順番。公式だと、引数→期待の順番。だが、引数は複数あり得るのだから、期待→引数の順番がわかりやすい。assertの順番ともあうし。
メソッドは、test→provideの順番に書くといいみたい。testメソッドに対して、テストケースのデータを流すから。メソッドが先で、データが後。どういう試験をするかが、メソッドで先にわかるイメージの模様。
テストメソッドの名前は、test<nethod name><テスト観点> みたいな感じにすると良い。次のResolutionのように、1メソッドに対して、違うテスト観点で複数テストすることがあるから。
Resolution
https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83
data providerを使って渡すテストデータは、同じ観点にする。
例えば、表示の試験は、表示のバリエーション。表示の他に、ボタンの動作試験をしたい場合、違うテストメソッド、data providerにしたらしい。
1個のdata providerで複数の観点の試験もできなくはないが、引数とテストメソッドが複雑になって、わかりにくくなる。
3. The Command-Line Test Runner
3. The Command-Line Test Runner — PHPUnit 9.6 Manual
phpunitのコマンド自体の使用方法。
phpunit <file> phpunit <directory>
引数にテスト対象ファイルの相対パスか、ディレクトリーを指定する。ディレクトリーを指定した場合、配下の全*Test.phpを実行する。
基本はphpunit testsを実行すればいいと思う。
Fixtures
出典: 4. Fixtures — PHPUnit 9.6 Manual。
テストメソッドの実行前に、テスト対象のインスタンスの生成や、DB接続など準備がいろいろある。これをFixturesと呼んでいる。この準備がけっこう手間になる。これを省力できるのがテストフレームワークの利点。
テストメソッド実行前後に共通で行える処理がある。
- setUp/tearDown: テストメソッド単位の前後処理。テスト対象インスタンスの生成など。tearDownは何もしなくてもいいことが多い。
- setUpBeforeClass/tearDownAfterClass: クラス単位の前後処理。 DB接続など。
共通クラスの初期化処理
https://chatgpt.com/share/6836d32d-e9b8-800b-9433-444ff1a1e2bf
PHPUnitのTestCaseを継承した独自の共通処理クラスを作る場合。初期化処理は__constructでやってはいけないらしい。
テスト用にいろいろ特別な初期化をしているから。
代わりに、setUpで共通で使うインスタンス生成などをする。子クラスではparent::setUp()が毎回必要になるがしかたない。
XML Configuration File
出典:
基本はコマンドでテスト対象クラス・ファイルを指定してテストを実行する。他に、XMLの設定ファイル (phpunit.xml) でもテスト対象を指定できる。
ただし、phpunit.xmlかphpunit.xml.distがあって、かつ--configurationの指定がない場合、これらのファイルを自動的に読み込む。
testsディレクトリーの全*Test.phpを対象にする最小限の例は以下。
<phpunit bootstrap="src/autoload.php">
<testsuites>
<testsuite name="money">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
phpunit.xmlがあれば、単にphpunitコマンドを実行するだけでいい。
phpunit --bootstrap src/autoload.php --testsuite money
上記のコマンド相当になる。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="tests">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
https://chatgpt.com/share/6821bc52-3324-800b-a98c-cbefe6c42324
| element | attribute | default | |
|---|---|---|---|
| phpunit | bootstrap | - | --bootstrap相当。テスト実行前の読込スクリプト。 |
| colors | false | true=--colors=auto相当、false=--colors=never相当。 | |
| verbose | |||
| testsuites | - | - | testsuiteの親要素。 |
| testsuite | - | - | name属性が必須で、テスト検索用の1以上のdirectoryかfile要素が必要。 |
| testsuite | name | 必須。ディレクトリー名でいい気がする。 | |
| phpunit/php | PHPの設定。 | ||
| phpunit/php/includePath | include_pathの先頭に追加するパス。 | ||
| phpunit/php/ini | name/value | ||
| phpunit/php/const, var | グローバルな定数、変数を設定。 | ||
| phpunit/php/env | name/value | 環境変数。 | |
| phpunit/php/get, post, cookie, server, files, request | name/value | 該当するスーパーグローバル変数の設定。 |
GuzzleのHTTPクライアントでプロキシー設定無視で以下の設定はよく使うかもしれない。
<php><env name="NO_PROXY" value="*"/></php>
bootstrap
https://chatgpt.com/share/6821bc52-3324-800b-a98c-cbefe6c42324
autoloadを使っていないプロジェクトだと、手動でプロジェクトルートパスなどの指定が必要になる。
tests以下にbootstrap.phpを用意して、その中で必要なものを読み込む形にするといい。
<?php
// tests/bootstrap.php
// $additionalPath = PROJECT_ROOT . '/src'; set_include_path(get_include_path() . PATH_SEPARATOR . $additionalPath); set_include_path(implode(PATH_SEPARATOR, [ get_include_path(), PROJECT_ROOT . '/src', PROJECT_ROOT . '/lib' ]));
// Composerのオートローダー
require_once __DIR__ . '/../vendor/autoload.php';
// オートローダーでは読み込めない独自ファイル
require_once __DIR__ . '/manual-loads/special-loader.php';
// 必要なら定数定義
define('PROJECT_ROOT', dirname(__DIR__));
<phpunit bootstrap="tests/bootstrap.php">
Assertions
About
Ref:
基本はassertSameでテストすればいいのだが、それ以外にも例外とかいろいろ試験したいケースがあるので、メソッドを整理する。
Method
インスタンスメソッドとstaticメソッドの2種類がある。他に、グローバル関数もある。どれを使ってもいい。
- アサーションメソッド: PHPUnit\Framework\Assert
- PHPUnit\Framework\TestCaseはAssertを継承している。
- phpunit/phpunit/src/Framework/Assert/Functions.php でグローバル関数。
入力文字数や好みの問題。グローバル関数は内部的にstaticメソッドを呼んでいる。
TestCaseのインスタンスメソッドの方が違和感がないという説がある。
$this->よりもself::のほうが短い。グローバル関数は名前空間がないのがまずい。self::のstaticメソッドでいい気がする。
Negate
assert系メソッドは、assertNotでNotを前置したら否定形になる。assertStringとかグループがある場合は、assertStringNotのように、グループの後にNotになる。
同値判定
- assertSame/assertNotSame: 型と値の両方を検査 (===相当)。
- assertEquals: 値を検査 (==相当)。
オブジェクトの属性値の値が同じかどうかをみたいなら、assertEqualsになる。assertSameは参照 (ポインター) のアドレス値の比較みたいなことをするから。
基本はassertSameでよいと思われる。
- assertEqualsCanonicalizing()
- assertEqualsIgnoringCase()
- assertEqualsWithDelta()
他にこういうバリエーションがある。
論理判定
- assertIsBool
- assertFalse
- assertTrue()
数値判定
- assertGreaterThan
- assertGreaterThanOrEqual
- assertLessThan
- assertLessThanOrEqual
- assertInfinite
- assertNan
- assertIsInt
- assertIsNumeric
- assertIsFloat
文字列判定
- assertIsString
- assertMatchesRegularExpression
- assertStringContainsString
- assertStringContainsStringIgnoringCase()
- assertStringMatchesFormat
- assertStringMatchesFormatFile
- assertStringEndsWith
- assertStringEqualsFile
- assertStringStartsWith
配列判定
配列の検査用のメソッドがある。
- 要素数
- assertEmpty
- assertSameSize(): 2個の配列の要素数が同じか。
- assertCount(): 指定した配列の要素数が指定数か。データの取得数などでこちらをよく使いそう。
- 包含
- assertContains
- assertContainsOnly()
- assertContainsOnlyInstancesOf()
- assertArrayHasKey
- assertIsArray
- assertIsIterable
クラス
- assertClassHasAttribute
- assertClassHasStaticAttribute
- assertObjectEquals
- assertInstanceOf
- assertIsCallable
- assertIsObject
- assertIsResource
- assertIsScalar
- assertNull
- assertObjectHasProperty
ファイル
- assertDirectoryExists
- assertDirectoryIsReadable
- assertDirectoryIsWritable
- assertFileEquals
- assertFileExists
- assertFileIsReadable
- assertFileIsWritable
- assertIsReadable
- assertIsWritable
その他
- assertThat
- assertJsonFileEqualsJsonFile
- assertJsonStringEqualsJsonFile
- assertJsonStringEqualsJsonString
- assertXmlFileEqualsXmlFile
- assertXmlStringEqualsXmlFile
- assertXmlStringEqualsXmlString
Exception
2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual
特に例外の試験がイレギュラー。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ExceptionTest extends TestCase
{
public function testException(): void
{
$this->expectException(InvalidArgumentException::class);
// Run test target code following.
}
}
上記のようにexpectExceptionを使う。
- expectException:
- expectExceptionCode:
- expectExceptionMessage:
- expectExceptionMessageMatches:
例外が発生する処理の前に記述しておく。
Testing Output
2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual
echoなど標準出力を試験する際も専用のメソッドがある。
void expectOutputRegex(string $regularExpression)void expectOutputString(string $expectedString)bool setOutputCallback(callable $callback)string getActualOutput()
expectExceptionと同様に事前にセットしておく。
Constraint
1. Assertions — PHPUnit 9.6 Manual assertThatとwithで使えるConstraint。通常のassertと似たような名前のものも多いので注意する。equalToとかはConstraintにしかない。PHPUnit\Framework\Constraintの名前空間の各種クラスのメソッドになる。
- 論理
- isFalse
- isTrue
- logicalAnd
- logicalNot
- logicalOr
- logicalXor
- 数値
- greaterThan
- greaterThanOrEqual
- lessThan
- lessThanOrEqual
- 文字列
- matchesRegularExpression
- stringContains
- stringEndsWith
- stringStartsWith
- 配列
- arrayHasKey
- contains
- containsOnly
- containsOnlyInstanceOf
- クラス
- classHasAttribute
- classHasStaticAttribute
- objectHasAttribute
- isInstanceOf
- isNull
- ファイル
- directorExists
- fileExists
- isReadable
- isWritable
- その他
- isType
- anything
- equalTo
- identicalTo
上記にない制約は$this->callbackで自分で定義する。引数に引数がきて、true/falseを返す。
| Constraint | Meaning |
|---|---|
PHPUnit\Framework\Constraint\IsAnything anything()
|
Constraint that accepts any input value. |
PHPUnit\Framework\Constraint\ArrayHasKey arrayHasKey(mixed $key)
|
Constraint that asserts that the array has a given key. |
PHPUnit\Framework\Constraint\TraversableContains contains(mixed $value)
|
Constraint that asserts that the array or object that implements the Iterator interface contains a given value.
|
PHPUnit\Framework\Constraint\TraversableContainsOnly containsOnly(string $type)
|
Constraint that asserts that the array or object that implements the Iterator interface contains only values of a given type.
|
PHPUnit\Framework\Constraint\TraversableContainsOnly containsOnlyInstancesOf(string $classname)
|
Constraint that asserts that the array or object that implements the Iterator interface contains only instances of a given classname.
|
PHPUnit\Framework\Constraint\IsEqual equalTo($value, $delta = 0, $maxDepth = 10)
|
Constraint that checks if one value is equal to another. |
PHPUnit\Framework\Constraint\DirectoryExists directoryExists()
|
Constraint that checks if the directory exists. |
PHPUnit\Framework\Constraint\FileExists fileExists()
|
Constraint that checks if the file(name) exists. |
PHPUnit\Framework\Constraint\IsReadable isReadable()
|
Constraint that checks if the file(name) is readable. |
PHPUnit\Framework\Constraint\IsWritable isWritable()
|
Constraint that checks if the file(name) is writable. |
PHPUnit\Framework\Constraint\GreaterThan greaterThan(mixed $value)
|
Constraint that asserts that the value is greater than a given value. |
PHPUnit\Framework\Constraint\LogicalOr greaterThanOrEqual(mixed $value)
|
Constraint that asserts that the value is greater than or equal to a given value. |
PHPUnit\Framework\Constraint\ClassHasAttribute classHasAttribute(string $attributeName)
|
Constraint that asserts that the class has a given attribute. |
PHPUnit\Framework\Constraint\ClassHasStaticAttribute classHasStaticAttribute(string $attributeName)
|
Constraint that asserts that the class has a given static attribute. |
PHPUnit\Framework\Constraint\ObjectHasAttribute objectHasAttribute(string $attributeName)
|
Constraint that asserts that the object has a given attribute. |
PHPUnit\Framework\Constraint\IsIdentical identicalTo(mixed $value)
|
Constraint that asserts that one value is identical to another. |
PHPUnit\Framework\Constraint\IsFalse isFalse()
|
Constraint that asserts that the value is false.
|
PHPUnit\Framework\Constraint\IsInstanceOf isInstanceOf(string $className)
|
Constraint that asserts that the object is an instance of a given class. |
PHPUnit\Framework\Constraint\IsNull isNull()
|
Constraint that asserts that the value is null.
|
PHPUnit\Framework\Constraint\IsTrue isTrue()
|
Constraint that asserts that the value is true.
|
PHPUnit\Framework\Constraint\IsType isType(string $type)
|
Constraint that asserts that the value is of a specified type. |
PHPUnit\Framework\Constraint\LessThan lessThan(mixed $value)
|
Constraint that asserts that the value is smaller than a given value. |
PHPUnit\Framework\Constraint\LogicalOr lessThanOrEqual(mixed $value)
|
Constraint that asserts that the value is smaller than or equal to a given value. |
logicalAnd()
|
Logical AND. |
logicalNot(PHPUnit\Framework\Constraint $constraint)
|
Logical NOT. |
logicalOr()
|
Logical OR. |
logicalXor()
|
Logical XOR. |
PHPUnit\Framework\Constraint\PCREMatch matchesRegularExpression(string $pattern)
|
Constraint that asserts that the string matches a regular expression. |
PHPUnit\Framework\Constraint\StringContains stringContains(string $string, bool $case)
|
Constraint that asserts that the string contains a given string. |
PHPUnit\Framework\Constraint\StringEndsWith stringEndsWith(string $suffix)
|
Constraint that asserts that the string ends with a given suffix. |
PHPUnit\Framework\Constraint\StringStartsWith stringStartsWith(string $prefix)
|
Constraint that asserts that the string starts with a given prefix. |
Command-Line
Ref: 3. The Command-Line Test Runner — PHPUnit 9.6 Manual.
phpunitコマンドでいろいろできる。いくつか重要なオプション、使用方法がある。
- phpunit file.php: 指定したファイルのテストを実行。
- --testsuite <name>: テストを指定。
8. Test Doubles
Ref: 8. Test Doubles — PHPUnit 9.6 Manual.
About
テスト時に、依存関係を模擬したもので置換したいことがある。PHPUnitにそういう仕組が用意されている。
「テストダブル - Wikipedia」Doubleは代役、影武者を意味する。ロックマンX4のダブルはたぶんこの英単語が由来だろう。
テスト対象クラスのプロパティーのクラスやメソッドの代用品をテストダブルと呼ぶ。テストダブルで置換するというような言葉の使い方をすると思われる。
テストダブルには、stubとmockがある。
- Stub: SUT内メソッド戻り値の検証
- Mock: SUT内メソッド引数の検証。
メソッド内で他のクラス・メソッドを使う場合はmockで対象クラスを模擬させる。
PHPUnitではテストダブル用に3の基本APIがある。
- createStub
- createMock
- getMockBuilder
引数に置換対象のクラスを指定してインスタンスを作成する。createStubとcreateMockは対象を全部置換する。getMockBuilderはこれら2種のメソッドを含んでいて、置換するメソッド・プロパティーを自分で選択できる。特定メソッドだけを置換して、他はそのまま使いたい場合、これを使うしかない。
Stub
オブジェクトのメソッドの戻り値を、テストダブルに置換する手法をスタブと呼ぶ。メソッド内の特定メソッドの戻り値を模擬することで、テストと関係ない処理の影響を無視できる。
実際に使う際は、テスト対象クラスのインスタンス作成後に、インスタンスがのプロパティーに設定して、インスタンス内の別クラスメソッド呼び出しを模擬する。
<?php declare(strict_types=1);
class SomeClass
{
public function doSomething()
{
// Do something.
}
}
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubTest extends TestCase
{
public function testStub(): void
{
// Create a stub for the SomeClass class.
$stub = $this->createStub(SomeClass::class);
// Configure the stub.
$stub->method('doSomething')
->willReturn('foo');
// Calling $stub->doSomething() will now return
// 'foo'.
$this->assertSame('foo', $stub->doSomething());
}
}
単に、メソッドの戻り値を模擬したいだけなら、expectsとwithは不要。method()->willReturnだけで十分。
| short hand | longer syntax |
|---|---|
willReturn($value)
|
will($this->returnValue($value))
|
willReturnArgument($argumentIndex)
|
will($this->returnArgument($argumentIndex))
|
willReturnCallback($callback)
|
will($this->returnCallback($callback))
|
willReturnMap($valueMap)
|
will($this->returnValueMap($valueMap))
|
willReturnOnConsecutiveCalls($value1, $value2)
|
will($this->onConsecutiveCalls($value1, $value2))
|
willReturnSelf()
|
will($this->returnSelf())
|
willThrowException($exception)
|
will($this->throwException($exception))
|
willReturnCallbackで呼び出し関数をまるごと別のものに置換できる。これが非常に便利。
引数を指定したい場合、with(引数)->willReturn()がシンプル。ただし、この方法は最後に指定した引数1個しか対応できない。引数違いで戻り値を模擬したいなら、willReturnMapかwillReturnCallbackで、引数に応じた戻り値を設定する。
willReturnCallbackのcallbackに渡される引数の記載はないが、モック対象メソッドに渡される引数が、その順番で渡される動きになっている。
Mock Objects
https://chatgpt.com/share/683e72cc-461c-800b-a7fc-5e8b417a44aa
メソッドが呼び出されたかどうかを検証するための、テストダブルへの置換をモッキングと呼ぶ。
テスト対象メソッド内で、特定メソッドの呼び出しを検査できる。MVCでViewへの値渡しなんかの検査にうってつけ。
モックオブジェクトにはテストスタブの機能も含んでいる。が、モッキングのための専用処理があるので、インスタンス生成時などのコストがやや大きい。理由がなければ、Stubを使った方がいい。
判断基準として、expects/withを使うならMock、そうじゃなければStubのようなイメージ。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
class Subject
{
protected $observers = [];
protected $name;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function attach(Observer $observer)
{
$this->observers[] = $observer;
}
public function doSomething()
{
// Do something.
// ...
// Notify observers that we did something.
$this->notify('something');
}
public function doSomethingBad()
{
foreach ($this->observers as $observer) {
$observer->reportError(42, 'Something bad happened', $this);
}
}
protected function notify($argument)
{
foreach ($this->observers as $observer) {
$observer->update($argument);
}
}
// Other methods.
}
class Observer
{
public function update($argument)
{
// Do something.
}
public function reportError($errorCode, $errorMessage, Subject $subject)
{
// Do something
}
// Other methods.
}
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class SubjectTest extends TestCase
{
public function testObserversAreUpdated(): void
{
// Create a mock for the Observer class,
// only mock the update() method.
$observer = $this->createMock(Observer::class);
// Set up the expectation for the update() method
// to be called only once and with the string 'something'
// as its parameter.
$observer->expects($this->once())
->method('update')
->with($this->equalTo('something'));
// Create a Subject object and attach the mocked
// Observer object to it.
$subject = new Subject('My subject');
$subject->attach($observer);
// Call the doSomething() method on the $subject object
// which we expect to call the mocked Observer object's
// update() method with the string 'something'.
$subject->doSomething();
}
}
基本的な作り。
- createMock(<class>::class)で該当クラスのモックを作成。
- expectsに呼出回数条件のオブジェクトをセット。これはなくてもいい。
- methodで対象メソッドを指定。
- withで該当メソッドの引数の検証条件を指定。
- willReturnなどで戻り値を指定。
デフォルトで模擬実装はnullを返す。戻り値を変更したければ、will($this->returnValue())などを指定する。よく使うので短縮記法もある。
Matcher
使えるメソッド。[https://github.com/sebastianbergmann/phpunit/blob/main/src/Framework/TestCase.php#L959] あたりからの一連のメソッドと思われる。
- any
- never
- atLeast
- once
- exactly (旧at)
- atMost
| Matcher | Meaning |
|---|---|
PHPUnit\Framework\MockObject\Matcher\AnyInvokedCount any()
|
Returns a matcher that matches when the method it is evaluated for is executed zero or more times. |
PHPUnit\Framework\MockObject\Matcher\InvokedCount never()
|
Returns a matcher that matches when the method it is evaluated for is never executed. |
PHPUnit\Framework\MockObject\Matcher\InvokedAtLeastOnce atLeastOnce()
|
Returns a matcher that matches when the method it is evaluated for is executed at least once. |
PHPUnit\Framework\MockObject\Matcher\InvokedCount once()
|
Returns a matcher that matches when the method it is evaluated for is executed exactly once. |
PHPUnit\Framework\MockObject\Matcher\InvokedCount exactly(int $count)
|
Returns a matcher that matches when the method it is evaluated for is executed exactly $count times.
|
PHPUnit\Framework\MockObject\Matcher\InvokedAtIndex at(int $index)
|
Returns a matcher that matches when the method it is evaluated for is invoked at the given $index.
|
Mock builder
https://chatgpt.com/share/68424beb-e544-800b-9292-b79f89cf412d
createStubやcreateMockは対象クラスを全部置換する。一部だけ置換したり、もともと存在しないメソッドを追加したり、複雑なことをしたい場合、getMockBuilderで取得する、Mock Builderを使う必要がある。メソッド一覧は以下となる。
onlyMethods(array $methods)Mock Builderオブジェクトで呼び出すことで、設定可能なテストダブルに置き換えるメソッドを指定できます。他のメソッドの動作は変更されません。各メソッドは、指定されたモッククラス内に存在している必要があります。addMethods(array $methods)Mock Builderオブジェクトで呼び出すことで、指定されたモッククラスに(まだ)存在しないメソッドを指定できます。他のメソッドの動作は同じです。setMethodsExcept(array $methods)Mock Builderオブジェクトで を呼び出すことで、他のすべてのパブリックメソッドを置き換えながら、設定可能なテストダブルに置き換えないメソッドを指定できます。これは の逆の動作をしますonlyMethods()。setConstructorArgs(array $args)元のクラスのコンストラクターに渡されるパラメーター配列を提供するために呼び出すことができます (デフォルトではダミー実装に置き換えられません)。setMockClassName($name)生成されたテストダブルクラスのクラス名を指定するために使用できます。disableOriginalConstructor()元のクラスのコンストラクターへの呼び出しを無効にするために使用できます。disableOriginalClone()元のクラスのクローンコンストラクターの呼び出しを無効にするために使用できます。disableAutoload()__autoload()テストダブルクラスの生成中に無効にするために使用できます。
use PHPUnit\Framework\TestCase;
class MyClassTest extends TestCase
{
public function testAddColumnAddsNewColumn()
{
// fetchDataだけをスタブ化する
$stub = $this->getMockBuilder(MyClass::class)
->onlyMethods(['fetchData']) // ← ここでfetchDataだけモック化
->getMock();
// fetchDataが返す想定の配列をセット
$stub->method('fetchData')->willReturn([
['id' => 1, 'name' => 'Alice'],
['id' => 2, 'name' => 'Bob'],
]);
// addColumnを呼び出してテスト
$result = $stub->addColumn();
// 検証
$this->assertEquals('value', $result[0]['new_column']);
$this->assertEquals('value', $result[1]['new_column']);
}
}
上記のように、最終的にgetMock()でインスタンスを取得して、methodなどで戻り値を設定する。
MVC
https://chatgpt.com/share/68395ad2-678c-800b-b27a-19c58e3a3a0a
MVC系のアプリで、Viewにセットする値・変数をチェックしたいことがある。
この場合、ControllerでViewにセットしているメソッドを試験する。
$controller-><view>->assign() $controller->setApp()
これらのメソッドを、expects/withで引数を検査する。willReturnはいらない。assert系のメソッドも呼ばなくていい。expects/withで引数の検査をするイメージ。
expects
https://chatgpt.com/share/682e8fd6-376c-800b-a01e-faa832b0fd07
「8. Test Doubles — PHPUnit 9.6 Manual」でモックオブジェクトを作成時に、模擬したメソッドの設定時に、expectsを使っている。
このexpectsは該当メソッドが呼ばれたか、呼ばれなかったかを試験するためのもの。例えば、if文で条件分岐していて、その中で該当メソッドが呼ばれたかどうかをチェックできる。
一本道で、呼び出し有無を気にしなくていいなら、expectsは不要。if文の条件を気にしなくて、一緒に試験できるのが便利。
なお、expectsを使わない場合、同名で競合するので、methodという名前のメソッドが、モックオブジェクトにあってはいけない。ある場合、expects($this->any())でexpectsを挟む必要がある。
ただ、methodという名前のメソッドがあることは普通ないと思う。
interface MockObject extends Stub
{
public function expects(InvocationOrder $invocationRule): InvocationStubber;
}
これが定義な模様。引数にはInvocationOrderをとる。InvocationOrderは「phpunit/src/Framework/MockObject/Runtime/Rule/InvokedCount.php at main · sebastianbergmann/phpunit」などで継承されている。
with
モックオブジェクトのメソッドの引数の検証に使うメソッド。ソースコードは以下。
- phpunit/src/Framework/MockObject/Runtime/Interface/InvocationStubber.php at e075df0a9d89a824ba66e12a7c23618d1a42faf3 · sebastianbergmann/phpunit
- phpunit/src/Framework/MockObject/Runtime/InvocationStubberImplementation.php at e075df0a9d89a824ba66e12a7c23618d1a42faf3 · sebastianbergmann/phpunit
https://chatgpt.com/share/683d657e-8578-800b-8e71-b09dbce969dd
「8. Test Doubles — PHPUnit 9.6 Manual」に記載がある。
The
with()method can take any number of arguments, corresponding to the number of arguments to the method being mocked. You can specify more advanced constraints on the method’s arguments than a simple match.
モック対象メソッドの個数と同じ引数を受け取れる。
それぞれの引数の位置で、該当引数の検証条件を指定する。指定可能な内容は以下の3種類。
- リテラル値
- Constraint: 指定可能なConstraints (制約) は「1. Assertions — PHPUnit 9.6 Manual」にある。
- callback: callbackの引数は、検証対象の引数で、OKならtrueを返す。
配列要素数や、複数のConstraintを組み合わせたい場合などは、callbackを使うしかない。
setAppやassignなどで、同じメソッドで1番目の引数に応じて、2番目の内容が変わる場合、工夫が必要。
expectやwithでは検証しないで、willReturnCallbackを使うしかない。連想配列に、実際に渡ってきたキー・バリューのセットを格納して、実行後にまとめてassertするか、コールバック内でassertしてチェックする。
$mock->method('assign')
->willReturnCallback(function ($key, $val) {
switch ($key) {
case 'title':
PHPUnit\Framework\Assert::assertSame('マイページ', $val);
break;
case 'user':
PHPUnit\Framework\Assert::assertInstanceOf(User::class, $val);
break;
default:
PHPUnit\Framework\Assert::fail("Unexpected key: $key");
}
});
$assigned = [];
$mock->method('assign')
->willReturnCallback(function ($key, $val) use (&$assigned) {
$assigned[$key] = $val;
});
// テスト対象実行後にチェック
$this->assertSame('マイページ', $assigned['title'] ?? null);
$this->assertInstanceOf(User::class, $assigned['user'] ?? null);
willReturnCallback内のifでいい気がする。複雑なら後者のスパイ風で。
willReturnCallbackでやる場合、Viewに渡すメソッドのもともとの戻り値がnullだから問題ないが、そうでない場合は、ちゃんとreturnでダミーの値を返さないと、後続の処理で不都合出るので注意する。
withで検査する場合、self::assertSame()でメソッドの検証はせずに、単にテスト対象メソッドを呼び出す。呼び出すと、withで仕込んだものが呼ばれて、中で検証するイメージになる。
DI
https://chatgpt.com/share/683faf08-f374-800b-ac40-b96b427d7365
テスト対象クラスのプロパティーのインスタンスをテストダブルに置換する場合、注意が必要。だいたい、privateになっているから、DIで渡す前に、置換対象のメソッドの模擬を設定してから、コンストラクターに渡す必要がある。
面倒くさかったら、テストダブルインスタンスの作成+メソッド設定を共通メソッドにしてもよいかもしれない。が、テストダブルの戻り値は、DB取得結果とかだと大事なので、手間だが1個ずつやった方がいいかもしれない。
protected function prepareLoggerWithLogReturn($value): LoggerInterface
{
$logger = $this->createLoggerMock();
$logger->method('log')->willReturn($value);
return $logger;
}
$logger = $this->prepareLoggerWithLogReturn(true);
外部static/グローバル関数の模擬
https://chatgpt.com/share/68424beb-e544-800b-9292-b79f89cf412d
例えば、ログインユーザーIDなど、引数に渡すまでもない共通値の取得が、SUTのメソッド内にあったりする。ただ、こういうstaticやグローバルなメソッド・関数は、PHPUnitで置換が難しい。
回避方法がいくつかある。
- DIで対象staticメソッドクラスをプロパティーに持たせる。
- 1と似た考えで、ラッパークラスを作る。
- 対象クラス内で、ラッパーメソッド (protected) を用意する。
1や2が望ましいようだが、3のラッパーメソッド用意は簡単。ひとまず3でいい。
テストのためだけに意味ない関数を追加するように見える。が、「テストできないコードはそれだけで設計に問題がある」とも言える。そういうものと思っておくと良いらしい。
Other
Test private/protected
- php - Best practices to test protected methods with PHPUnit - Stack Overflow
- PHPUnitでprivateメソッドをテストする
- privateとprotectedメソッドをPHPUnitでテストする方法 #PHP - Qiita
クラスのprivate/protectedメソッドのテストには工夫が必要となる。
/**
* privateメソッドを実行する.
* @param object $sut テスト対象のインスタンス。
* @param string $method_name privateメソッドの名前。
* @param array $param privateメソッドに渡す引数。
* @return mixed 実行結果。
* @throws \ReflectionException 引数のクラスがない場合に発生.
*/
private function doMethod(object $sut, string $method_name, array $param): mixed
{
// ReflectionClassをテスト対象のクラスをもとに作る.
$reflection = new \ReflectionClass($sut);
// メソッドを取得する.
$method = $reflection->getMethod($method_name);
// アクセス許可をする.
$method->setAccessible(true);
// メソッドを実行して返却値をそのまま返す.
return $method->invokeArgs($sut, $param);
}
ReflectionClassを使って取得できる。上記の関数のFormController部分を試験対象のクラスに差し替えればOK。getProperty/getValueでprivateプロパティーも取得可能。
https://chatgpt.com/c/673ebadb-1638-800b-89f0-b3ed131317da
基本はpublicメソッドのみテストすべきという考え方。
class MyClass {
private function myPrivateMethod($a, $b) {
return $a + $b;
}
}
class MyClassTest extends PHPUnit\Framework\TestCase {
public function testMyPrivateMethod() {
$object = new MyClass();
// Reflectionを使ってprivateメソッドにアクセス
$reflection = new ReflectionClass($object);
$method = $reflection->getMethod('myPrivateMethod');
$method->setAccessible(true);
// メソッドを呼び出してテスト
$result = $method->invoke($object, 2, 3);
$this->assertEquals(5, $result);
}
}
Test header
Ref: unit testing - Test PHP headers with PHPUnit - Stack Overflow.
header関数を使用する場合、phpunitの標準出力と干渉して以下のエラーが出て試験できない。
Cannot modify header information - headers already sent by (output started at .../vendor/phpunit/phpunit/src/Util/Printer.php:138)
回避方法が2種類ある。
@runInSeparateProcess- phpunit --stderr
1個目のアノテーションをテストメソッドに指定すると別プロセスでの実行になる。ただ、プロセス生成は時間がかかるため、試験が多いと効率が悪い。
2個目のphpunitの出力を標準エラーに出力させる方法がシンプルで効率もいい。phpunit.xmlに stderr="true"を指定するとキー入力を省略できる。こちらで対応しよう。
Test exit
Ref:
- PHP でテストコードを意識したコーディング #PHPUnit - Qiita
- echo + exit しているPHPコードをユニットテストで保護しながら改善する #PHP - Qiita
- header後にdieするテストのアンチパターン - uzullaがブログ
- php - Ignore exit() and die() with PHPUnit - Stack Overflow
- unit testing - How do you use PHPUnit to test a function if that function is supposed to kill PHP? - Stack Overflow
header()後のexit()など、exit/dieを使用するコードがある。phpunit内でこれらがあると、テストも強制終了になる。
上記の別プロセスで実行していた場合、以下のエラーになる。
Test was run in child process and ended unexpectedly
対処方法がいくつかある。
- exitを使わないコードに変更。
- isTestのようなフラグを元コードに入れてテスト可否で分岐してexitを回避。
- execで外部プロセスで実行してexitCodeを試験。
- exit/die部分だけ別関数に抽出してmockで置換?
<https://notabug.org/gnusocialjp/gnusocial/src/main/actions/apiaccountregister.php> のclientErrorが内部でexitする。
このclientErrorをwillなどで置換すればよさそう?
sut
https://chatgpt.com/share/682ada62-d3e0-800b-9444-db245dc44183
試験対象のクラスをメンバー変数・プロパティーに格納する。その際の名前を何にするか?
instance/classとかが思いつく。対話AIによると、sutというのがいいらしい。system under testの略。テスト対象の意味。試験の専門用語っぽい。短いのでこれでいいと思う。
表示内容の試験
画面UIに該当文字列があるかどうかなどを試験したいことがある。
LaravelにはassertSeeというのがあるのでこれを使える。
PHPUnit自体にはない。assertStringContainsStringなど、文字列試験メソッドを使って、自分でresponseを何かで取得して評価する。こういうのは基本は機能試験で行う内容。
単体試験と機能試験
https://chatgpt.com/share/682ada62-d3e0-800b-9444-db245dc44183
単体試験と機能試験がある。
単体試験は、基本的にクラス単位。クラスのメソッドを試験するイメージ。
機能試験は、特定機能に関する、クラス・メソッドを試験する。1個の試験で、複数クラスを試験する違いがある。
実際のアプリ開発では、ユーザー動作や仕様の動作が重要だから、機能試験中心で問題ない気がする。
重要なところ、複雑なところ、バグの多いところをUnitTestで試験するのがいいと思う。メンテ不能なテストができてしまうのは避けたい。
- tests
- Unit
- Feature
Unitは元ファイルのディレクトリー構成にして、Featureは機能単位。Featureで機能単位の試験を入れる。で、基本はFeatureを拡充させる。
ファイル名・クラス名。Featureの方はXXFreatureTest.phpとかにすることが多いらしい。が、せっかくディレクトリー分けている意味がないので、Test.phpでいいでしょう。
result cache
- php - What is .phpunit.result.cache - Stack Overflow
- 3. The Command-Line Test Runner — PHPUnit 9.6 Manual
- 3. The XML Configuration File — PHPUnit 9.6 Manual
- phpunit/src/Runner/ResultCache/DefaultResultCache.php at 42966d97ce3b66e65beb46411b4f72bf467746f2 · sebastianbergmann/phpunit
- phpunit/src/TextUI/Configuration/Merger.php at 42966d97ce3b66e65beb46411b4f72bf467746f2 · sebastianbergmann/phpunit
phpunitを実行すると、.phpunit.result.cacheファイルが作成される。
phpunit.xmlのcacheResultがデフォルトtrueになっており作成されている。
- phpunit.xml
- phpunit.cacheResult: 初期値true。結果キャッシュ作成有無。
- phpunit.cacheResultFile: 初期値.phpunit.result.cache。ファイルパス。
- option
- --cache-result: キャッシュ結果を出力する (既定)。
- --do-not-cache-result: キャッシュ結果を出力しない。
- --cache-result-file <file>: キャッシュ結果のパスを指定する (既定: ./.phpunit.result.cache)。
cacheResultFileは他にPHPUNIT_RESULT_CACHE環境変数でも設定できる模様。
phpstanの結果キャッシュをvar/tmp/phpstanに配置しているので、これにならってvar/tmp/phpunit/.phpunit.result.cacheにするといいかも。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php" cacheResultFile="var/tmp/phpunit/.phpunit.result.cache" colors="true">
<testsuites>
<testsuite name="tests">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
getallheaders()
Error: Call to undefined function getallheaders()
apache_request_headers()のエイリアス。サーバー固有のAPIはPHPUnit実行時は使えないので、bootstrap.phpに、代替関数を用意する。
if (!function_exists('getallheaders')) {
function getallheaders() {
$headers = [];
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
}
PHP Fatal error: Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in
PHP Fatal error: Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in /common/php/src/PHPUnit/Framework/SelfDescribing.php on line 58
Fatal error: Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in /common/php/src/PHPUnit/Framework/SelfDescribing.php on line 58
while running parallel worker
古いPHPUnitがある環境に、新しめのPHPUnitがある状態で、PHPStanを実行すると上記のエラーが出てしまった。
https://chatgpt.com/share/6837b3e4-01ac-800b-a547-164fcca8dbf8
古いPHPUnitと新しいPHPUnitとで、同じクラスでも指定方法が変わっている。その都合で、両方がinclude_pathにあると競合する。include_pathから除外する必要がある。
テストのグループ化
https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83
Jestにはdescribe()内にit()を入れるような、テストメソッドのネスト構造ができた。PHPUnitにはそれがない。
@groupアノテーションがあるが、これは--groupや--exclude-groupで指定したグループに属するテストを対象にしたり、除外したりするためのもの。ネストとは概念が異なる。
ネスト構造はクラスとメソッドの親子関係での表現になる。分けたかったら、テストメソッドの命名規則などになる。
親クラスBaseTestCase
テストクラスで、共通の処理とかしたいことがある。親クラスにまとめたい。
PHPUntはTestCaseがテスト対象じゃないことを意味するので、親クラスも最後はTestではなくてTestCaseにする。
https://grok.com/share/c2hhcmQtMw%3D%3D_0cfa311d-b290-4d5b-9183-ca7f7efb2d53
BaseTestCaseとか。
共通テストメソッドはassertXxxにする。testXxxはPHPUnitのtest対象になるので。
プロセス
https://grok.com/share/c2hhcmQtMw%3D%3D_e1586c8a-b519-44be-b04d-185e61969ae2
phpunitは基本的に同一プロセスで全テストを実行する。その都合で、あるファイルでincludeしたシンボルはグローバルに存在するので、他のファイルにも影響ある。
あるファイルAでincludeしてシンボルAAが登場して、別のファイルBでincludeするファイル内にシンボルBBがある場合、エラーになる。
対策がいくつかある。
- 名前空間を活用してシンボル衝突を防ぐ。
- phpunit --process-isolationのオプションを指定。ただし、遅くなる。
- テストスイートの分離。
根本的には、同じシンボルが複数ファイルでグローバルで登場するのがまずい。名前空間、require_once、function_existsとかでガードすべき。元ファイルが想定していないなら、--process-isolationオプションを使う。
