Thanks Driven Life

日々是感謝

PHP 5.4 〜 PHP 8.2 をサポートする PHP パッケージの PHPUnit 環境を整えた話

モチベーション

なぜ整えるに至ったかについてです。

私はこのパッケージの作者兼メンテナーです。

github.com

(以降 本パッケージ と記載します)

PHP 5.4 で廃止された register_globalsmagic_quotes_gpcPHP 5.4 以上でも似たような形で再現してくれる君です。

つまり PHP 5.4以上で動いてほしいパッケージ であり、メンテナーとしても PHP 5.4 以上で使える状態にしておきたい のです。EOL を迎えているとしてもあえてサポートする構えなのです*1


PHP の各バージョンをサポートするにあたり、やはり自動テストは欠かせないでしょう。PHP パッケージであれば PHPUnit ですよね。 突然ですが下図をご覧ください。使用する PHP バージョン毎に、対応する PHPUnit のバージョンも異なることがわかります。

PHPUnitがサポートしているPHPのバージョン一覧絵

Supported Versions of PHPUnit – The PHP Testing Framework

これらを踏まえて、こう考えました。 ( 🔥 が本記事のメインテーマです )

  • やりたいこと
    • 本パッケージのアプリケーションコードは PHP 5.4 から PHP 8.2 (2023年8月現在) まで動く構文のみで実装している
    • ↑ を保証するために、PHP の各バージョンで自動テストを走らせたい。PHPUnit
  • やりたいことを達成するまでの懸念
    • PHPUnit はバージョン毎に(当然だが)サポートする PHP バージョンや機能があったり無かったりする
    • なるべく PHPUnit のバージョンは上げたいが、そうもいかない
      • 例:とある機能が PHPUnit 4 で動かないので PHPUnit 5 に上げたいけど、そうすると PHP 5.4 をテストできない
    • 🔥 つまりアプリケーションコードだけでなく、テストコードも 同じコードのままあらゆるバージョンのPHPUnitで動くように揃えたい
      • テストコードは PHPUnit のバージョンに合わせて複数用意する手もあるが、メンテコストがやばそうなので、最終手段としたい

本記事について

PHP 5.4 から PHP 8.2 (PHPUnit で言えば 4 から 9) までを 同じアプリケーションコード、同じテストコードのまま CI に乗せるための試行錯誤の記録となります。

期待値調整

まず PHP のバージョン差異による対応(※1)については今回主題ではないので書いていません。あくまで PHPUnit の話だけを書いています。

※1 例えば配列の分割代入で、list()じゃなくて[]使えるようになったのはPHP 7.1からだよ、とかそういうやつ


小さいパッケージなので、対応したこと自体はそこまで多くありません。そのためタイトルに書いてある状況と似た 他のパッケージすべてに適用できるような内容ではありません

?「うちのパッケージで同じことやろうとしたけど、コレも足りひんアレも足りひんで、この記事の内容だけだと全然網羅できてなかったやん!」 僕「それはそう」


本記事で書いた対応は 「これがベスト!」というものでも無い と考えています。より良い解決方法をお持ちの方は是非コメントお願いします。

対応リスト

具体的な実装は Release Release v0.0.6 · gongo/merciful-polluter · GitHub に全て含まれています。個別でもリンクを貼っておきます。

1. PHPUnit 4 → PHPUnit 6 (PHP 7.2 以降では必須)

課題

PHPUnit 6 で大きく変わったことといえば様々ありますが、今回は以下2つを見ていきます。

  • PHP 7.0 以上を要件とするようになった
  • PHPUnit のクラスが名前空間付きで定義されるようになった

Release Announcement for Version 6 of PHPUnit – The PHP Testing Framework

前者については書いてあるとおり、後者については例えばこういうやつです。

  • PHPUnit 6 未満
    • PHPUnit_Framework_TestCase
  • PHPUnit 6 以上
    • PHPUnit\Framework\TestCase

つまり下記のようなテストコードだと PHPUnit 7からは動かなくなる、ということです。

<?php
use \PHPUnit_Framework_TestCase;  // PHPUnit 7 だと存在しないクラス

class RequestTest extends PHPUnit_Framework_TestCase
{
}

それでは PHPUnit\Framework\TestCase に書き換えればいいじゃん! となるのですが

  • PHP 5.x は PHPUnit 5 以下でしか動かない
  • PHPUnit 5 ではまだ PHPUnit\Framework\TestCase が定義されていない

となってしまい、同じテストコードを維持したまま別々のバージョンの PHPUnit を走らせることができないのです!! 困った!!

対応

いくつか方法がありますが、早いのはこんな感じで。

  1. PHP 5.x で使う PHPUnit4.8.35 以上にする
    • PHPUnit 5系であれば 5.7.0 以上
  2. テストコードをこんな感じで書き換える
-use \PHPUnit_Framework_TestCase;
+use PHPUnit\Framework\TestCase;

-class RequestTest extends PHPUnit_Framework_TestCase
+class RequestTest extends TestCase
 {
 }

PHPUnit 4.8.35 から ForwardCompatibility/TestCase.php が用意されています。これを利用することで

  • PHPUnit 6 未満
    • PHPUnit\Framework\TestCasePHPUnit_Framework_TestCase を継承した abstract class
  • PHPUnit 6 以上
    • PHPUnit\Framework\TestCase はそのままのとおり

となります。つまり PHPUnit 6 未満でも以上でも同じテストコードで PHPUnit を実行できるということです。やったね!!

参考資料

  1. PHP5.3 対応でも PHPUnit は 6 スタイルの記述へ移行しよう - Qiita
  2. Use PHPUnit 7.5 for support PHP 7.4 by zonuexe · Pull Request #13 · gongo/merciful-polluter · GitHub
    • ForwardCompatibility が入っていない PHPUnit を使う場合は自分のリポジトリ内で同じコードを用意しよう、の例

2. PHPUnit 4 → PHPUnit 8 (PHP 8.0 以降では必須)

課題

PHPUnit 8 で大きく変わったことといえば様々ありますが、今回は以下2つを見ていきます。

  • PHP 7.2 以上を要件とするようになった
  • PHPUnit の一部のメソッドに戻り値の型が指定された

Release Announcement for Version 8 of PHPUnit – The PHP Testing Framework

前者については書いてあるとおり、後者については、こういうエラーが出るようになります。

PHP Fatal error:  Declaration of Gongo\MercifulPolluter\Test\BaseTest::setUp() must be compatible with PHPUnit\Framework\TestCase::setUp(): void

これはつまりテストコードで継承している setUp で戻り値の型が指定されていないが TestCase.setUp() では定義されているで! だから違うで! というものです。

<?php

use PHPUnit\Framework\TestCase;

class BaseTest extends TestCase
{
    // TestCase だと setUp(): void になっている
    protected function setUp() // ここは指定されていない
    {
    }
}

じゃあどうするかと考えますが、PHP 5.x だと戻り値の型指定がそもそもできないので、:void つけても syntax error で怒られていまいます。困った!!

対応

かなり妥協した案です。

Update PHPUnit (for PHP 8.0 or earlier) by gongo · Pull Request #17 · gongo/merciful-polluter · GitHub

そう、つまり 型指定されているメソッドを使わない です。正直かなり苦しい。

 class FooTest extends TestCase
 {
-    protected function setUp()
-    {
-        $this->object = new Foo;
-    }

     public function testFoo()
     {
+        $this->object = new Foo;
         $this->assertEquals('foo', $this->object->foo());
      }
 }

今回は setUp() だけで、対象となるテストメソッドもそんなに多くなかったので、ひたすら各テストメソッドの setUp() の内容を書いていくだけの作業を行いました。 これ以上の規模になってくると、テストコードを分けた方がいいんだろうなという気持ちにはなっています。

3. PHPUnit 4 → PHPUnit 9

課題

Release Announcement for Version 9 of PHPUnit – The PHP Testing Framework

今回の対象パッケージで引っ掛かったのはこちらの項目です。

Backward Incompatible Changes

The following functionality was removed:

  • Annotation(s) for expecting exceptions

アノテーションとは、今回の文脈でいうとこちらのコードです。

merciful-polluter/test/SessionTest.php at 0.0.5 · gongo/merciful-polluter · GitHub

  1. trigger_errorE_USER_WARNING を投げるコードがある
  2. PHPUnitE_USER_WARNING を例外に変換してしまう
  3. それを補足するために @expectedException アノテーションを書いておく

みたいな儀礼が存在しました。しかし PHPUnit 9 からはそのアノテーションによる補足機能を廃止することとなりました。困った!!

対応

代用としてか PHPUnit 9 から expectWarning() が誕生したので、そちらを使うようにしてみました。

<?php

    // (snip)

    /**
     * Below annotations are for PHPUnit < 9.0
     *
     * @expectedException PHPUnit_Framework_Error_Warning
     * @expectedExceptionMessage The session not yet started (Ignoring)
     */
    public function testPolluteSessionNotStarted()
    {
        // For PHPUnit >= 9.0
        if (method_exists($this, 'expectWarning')) {
            $this->expectWarning();
            $this->expectWarningMessage('The session not yet started (Ignoring)');
        }
        
        $this->object = new Session;
        $this->object->pollute();
    }

これでOK!!

参考資料

4. PHPUnit 4 → PHPUnit 10

課題

PHPUnit 10 に上げる必要性はこの時点ではなかったのですが、前項の対応を行った状態で PHPUnit を走らせると以下のような警告を貰います。

1) Gongo\MercifulPolluter\Test\SessionTest::testPolluteSessionNotStarted
Expecting E_WARNING and E_USER_WARNING is deprecated and will no longer be possible in PHPUnit 10.

PHPUnit 10 までの短い命だったのか……困った!!

対応

@expectedWarning アノテーションexpectWarning() を使わない方法、かつ PHP 5.x でも動く方法を発見しました。インターネットから。

github.com

trigger_error() が実行されそうな処理の直前で set_error_handler() をしておき、E_USER_WARNING を補足するという古き良き対策です。便利。

参考資料

結果

php5.4から8.2まですべてテストをpassしている

綺麗

まとめ

みんなちゃんと PHP バージョンアップしような! register_globalsmagic_quotes_gpc から逃れられなくて PHP 5.4 に上げられない人は、まあ頑張ってや!!

*1:使う人おらんやろという気持ちもあるので、そこまで高尚な心持ちではないです