「PHP tool」の版間の差分

提供:senooken JP Wiki
(3. The Command-Line Test Runner)
 
669行目: 669行目:
====Topic====
====Topic====
=====Test private/protected=====
=====Test private/protected=====
Ref:
*[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]
*[https://zenn.dev/ttskch/articles/c7dcd5c1188cdd PHPUnitでprivateメソッドをテストする]
*[https://zenn.dev/ttskch/articles/c7dcd5c1188cdd PHPUnitでprivateメソッドをテストする]
695行目: 694行目:
     }
     }
</syntaxhighlight>ReflectionClassを使って取得できる。上記の関数のFormController部分を試験対象のクラスに差し替えればOK。$this->instanceを指定しておけばそのまま流用できるか。getProperty/getValueでprivateプロパティーも取得可能。
</syntaxhighlight>ReflectionClassを使って取得できる。上記の関数のFormController部分を試験対象のクラスに差し替えればOK。$this->instanceを指定しておけばそのまま流用できるか。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=====
=====Test header=====
Ref: [https://stackoverflow.com/questions/9745080/test-php-headers-with-phpunit unit testing - Test PHP headers with PHPUnit - Stack Overflow].
Ref: [https://stackoverflow.com/questions/9745080/test-php-headers-with-phpunit unit testing - Test PHP headers with PHPUnit - Stack Overflow].

2024年11月21日 (木) 14:19時点における最新版

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の親クラスのときだけ継承される。


DocBlock

DocComments

DocBlockはDocCommentと呼ばれるコメントで囲まれる。DocCommentは/**で始まり、*/で終わる。そして、DocComment内の行の先頭は* で始まるべき。

<?php
/**
 * This is a DocBlock.
 */
function associatedFunction()
{
}

/** This is a single line DocComment. */

複数行形式と1行形式がある。

変数などの説明には1行形式でいいと思う。

PHPDoc

DocBlockは3部構成。

  1. Summary=短い説明。改行直前の.か空行で終わり。
  2. Description=長い説明。アルゴリズムの機能や、使用方法、例など。最初のタグか、改行、DocBlockの終端で終わる。
  3. 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
 */

Tag

よく使う@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: クラスの注釈部で指定する。メンバー変数の説明。
  • @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;
    }
}

Other

Array

PHPでよく使うArray。PHPDocでの表現方法がある。

  1. @return array
  2. @return int[]
  3. @return (int|string)[]

PHPDocでの配列の表現方法は以上。連想配列については説明なし。arrayしかない。

ArrayShape

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}
 * }
 */

Package manager

Composer

PHPのパッケージ管理システム。PHP v5.3.2以上で動作する。

Install

Ref: インストール: Composer | モダンなPHPのパッケージマネージャー – senooken JP.

[ -e installer ] || wget https://getcomposer.org/installer
php installer --install-dir="$LOCAL/stow/$PKG-$VER/bin" --filename=$PKG

公式サイトからinstallerをダウンロードしてそれを使って任意の場所に設置する。

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)

ライブラリーの自動読み込みのために,Composerはvendor/autoload.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欄に指定することで,ライブラリー以外の自分のコードも自動読み込みの対象にできる。

composer.jsonを編集した場合、composer dump-autoloadを実行してvendor/autolaod.phpを必ず更新します。

Libraries

Libraries - Composer

自前のライブラリーを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の方法をとるしかない。

  1. vendorディレクトリーをコミットに含める。
  2. リリース用zipにのみvendorを含める (例: Yoast/wordpress-seo: Yoast SEO for WordPress)。

Yoast SEOはGruntでリリース用に用意している模様。

composer.jsonにscriptsを含めることできるし、package.jsonでもOK。

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

PHPの静的解析ツール。

  • php -l
  • PHPStan
    • Larastan: LaravelでPHPStanを使うと、static呼び出しなどで大量のエラーが出ますので、それらをカバーしてくれます。
  • PHP Code Sniffer
  • PHPMD
  • Psalm
  • PhpStorm
  • Rector

上記が有名。

PHPStanとPsalmはベースラインを設定できるので既存のプロジェクトにも導入しやすい。

PHPのバージョンアップの互換性の確認などもできる。

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

外部ツールも不要で手軽で悪くない。特に、-lで実行するphpのバージョンを変えるだけでチェックでき、PHPのバージョンアップ時の事前の簡易チェック、粗探しとしてとして悪くない (PHPerのための「静的解析」を語り合う【PHP TechCafe イベントレポート】 - RAKUS Developers Blog | ラクス エンジニアブログ)。

PHPStan

導入が簡単なので黙って導入したらよさそう。

PHPUnit

PHPUnitを使用すれば自動単体テスト (Unit test) が可能だ。

Version

情報源: Supported Versions of PHPUnit – The PHP Testing Framework

PHPUnitのバージョンごとに対応しているPHPのバージョンが決まっている。

PHP v7.4に対応してい最後のバージョンはPHPUnit 9なので、当分はこれを使うのが良い。

Getting Started with PHPUnit 9

Getting Started with Version 9 of PHPUnit

  1. phpunitをインストール
  2. テストコード記述
  3. phpunit実行

上記の3ステップで使う。

以下のように引数に、ディレクトリーかテスト対象ファイルの相対パスを探す。

phpunit tests

Basic

出典: 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual

基本的な使用方法を整理する。

  1. 基本的にはクラス単位で試験コードを記載。<Class>クラスの試験コードは<Class>Testの命名にする。
  2. <Class>Test はPHPUnit\Framwork\TestCaseを継承させる。
  3. 試験はpublicのtest*メソッドの命名にする。あるいは、@testのアノテーションを付ければ、命名規則に従わなくてもいい。
  4. test*メソッド内で、assertSame() などで、期待値との比較で試験を行う。
  5. 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));
    }
}
Depends

前回の試験で準備した結果を利用したい場合、@dependsのアノテーションでテスト関数を指定しておくと、指定したテスト関数のreturnを引数に受け継いだテスト関数を記述できる。

@dependsは複数指定でき、指定順に事前に試験が実行されて引数に渡される。

Data Provider

ある関数に対して、テストケースを用意して、複数の引数の組み合わせを試験したいことがよくある。こういうときのために、テスト関数に渡す引数を生成する関数のデータプロバイダーを指定できる。@dataProviderで関数を指定する。データプロバイダーは引数のリストを配列で返すようにする。

データ数が多い場合、名前付き配列にしておくと、どういうデータ項目で失敗したかがわかりやすい。

Iteratorオブジェクトを返してもいい。

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接続など。
XML Configuration File

出典:

基本はコマンドでテスト対象クラス・ファイルを指定してテストを実行する。他に、XMLの設定ファイル (phpunit.xml) でもテスト対象を指定できる。

testsディレクトリーの全*Test.phpを対象にする最小限の例は以下。

<phpunit bootstrap="src/autoload.php">
  <testsuites>
    <testsuite name="money">
      <directory>tests</directory>
    </testsuite>
  </testsuites>
</phpunit>

以下のように--testsuiteで試験対象を指定して実行する。

phpunit --bootstrap src/autoload.php --testsuite money

Assertions

Ref:

基本はassertSameでテストすればいいのだが、それ以外にも例外とかいろいろ試験したいケースがあるので、メソッドを整理する。

Exception

特に例外の試験がイレギュラー。

<?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:

例外が発生する処理の前に記述しておく。

Output

echoなど標準出力を試験する際も専用のメソッドがある。

  • void expectOutputRegex(string $regularExpression)
  • void expectOutputString(string $expectedString)
  • bool setOutputCallback(callable $callback)
  • string getActualOutput()

expectExceptionと同様に事前にセットしておく。

Command-Line

Ref: 3. The Command-Line Test Runner — PHPUnit 9.6 Manual.

phpunitコマンドでいろいろできる。いくつか重要なオプション、使用方法がある。

  • phpunit file.php: 指定したファイルのテストを実行。
  • --testsuite <name>: テストを指定。

Test Doubles

Ref: 8. Test Doubles — PHPUnit 9.6 Manual.

テスト時に、依存関係を模擬したもので置換したいことがある。PHPUnitにそういう仕組が用意されている。

stub=親、mock=子。

メソッド内で他のクラス・メソッドを使う場合はmockで対象クラスを模擬させる。

<?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();
    }
}

基本的な作り。

  1. createMock(<class>::class)で該当クラスのモックを作成。
  2. expectsに呼出回数条件のオブジェクトをセット。
  3. methodで対象メソッドを指定。
  4. withで、該当メソッドの引数処理を指定。

デフォルトで模擬実装はnullを返す。戻り値を変更したければ、will($this->returnValue())などを指定する。よく使うので短縮記法もある。

Table 8.1 Stubbing short hands
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で呼び出し関数をまるごと別のものに置換できる。これが非常に便利。

Topic

Test private/protected

クラスのprivate/protectedメソッドのテストには工夫が必要となる。

    /**
     * privateメソッドを実行する.
     * @param string $methodName privateメソッドの名前
     * @param array $param privateメソッドに渡す引数
     * @return mixed 実行結果
     * @throws \ReflectionException 引数のクラスがない場合に発生.
     */
    private function doMethod(string $methodName, array $param)
    {
        // テスト対象のクラスをnewする.
        $controller = $this->instance;
        // ReflectionClassをテスト対象のクラスをもとに作る.
        $reflection = new \ReflectionClass($controller);
        // メソッドを取得する.
        $method = $reflection->getMethod($methodName);
        // アクセス許可をする.
        $method->setAccessible(true);
        // メソッドを実行して返却値をそのまま返す.
        return $method->invokeArgs($controller, $param);
    }

ReflectionClassを使って取得できる。上記の関数のFormController部分を試験対象のクラスに差し替えればOK。$this->instanceを指定しておけばそのまま流用できるか。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種類ある。

  1. @runInSeparateProcess
  2. phpunit --stderr

1個目のアノテーションをテストメソッドに指定すると別プロセスでの実行になる。ただ、プロセス生成は時間がかかるため、試験が多いと効率が悪い。

2個目のphpunitの出力を標準エラーに出力させる方法がシンプルで効率もいい。phpunit.xmlに stderr="true"を指定するとキー入力を省略できる。こちらで対応しよう。

Test exit

Ref:

header()後のexit()など、exit/dieを使用するコードがある。phpunit内でこれらがあると、テストも強制終了になる。

上記の別プロセスで実行していた場合、以下のエラーになる。

Test was run in child process and ended unexpectedly

対処方法がいくつかある。

  1. exitを使わないコードに変更。
  2. isTestのようなフラグを元コードに入れてテスト可否で分岐してexitを回避。
  3. execで外部プロセスで実行してexitCodeを試験。
  4. exit/die部分だけ別関数に抽出してmockで置換?

<https://notabug.org/gnusocialjp/gnusocial/src/main/actions/apiaccountregister.php> のclientErrorが内部でexitする。

このclientErrorをwillなどで置換すればよさそう?