Webエンジニアの日常とリーグオブレジェンド

Webエンジニアとして働いている猫のブログ。EmacsとMySQLとリーグオブレジェンド(LoL)が好物。主に技術的な記事かLoLの記事を書く。

PHPUnit 5 から PHPUnit 6 するときにハマった点とこれから

f:id:yoshiki_utakata:20191203191330p:plain

はじめに

この記事は PHP Advent Calendar 2019 - Qiita 4日目の記事です。

昨日の記事は tzmfreedom さんの PHPでPHPを実装する というすごいタイトルの記事でした。

去年のPHPアドベントカレンダーで PHP 5.6 から PHP 7.2 にする話を書きました。

www.utakata.work

この記事がめちゃめちゃ好評で、今でもたまに見られています。PHP 5.6 のサポートはとっくに終わったはずなのですが...

ということで今回は PHPUnit 5 を PHPUnit 6 に上げた時の話をします。ちなみに PHPUnit 6 はもうサポートが終了していますので、はやく PHPUnit 7 にする必要があります。俺たちの戦いは これからだ!

PHPUnit 6 にするきっかけ

先程の記事にもありましたが、PHP を 5.6 から 7.2 にする仕事をしていました。php 7.2 に対応するには最低でも PHPUnit 6 まで挙げなければなりません。

これに気づいた時、使用していた PHPUnit のバージョンは 4 でした。

PHPUnit 4 系の最新にすると、deprecatedな使い方をしている箇所でWarningメッセージが出るようになります。これでなんとか PHPUnit 5 にしたのですが、PHPUnit 5 にした瞬間... なんとその Warning の数 1000 個...

これを地道に潰していきます。

地道な修正作業

実際に PHPUnit 6 でテストを回しながら、落ちた箇所を一つ一つ直していきます。CI環境は PHPUnit 5 のままなので、 PHPUnit 5 で使えないモノは使わないよう注意します。

クラスの namespace

  • PHPUnit_Framework_Testcase
  • PHPUnit_Framework_TestListener
  • PHPUnit_Framework_Test

などのクラスが

  • PHPUnit\Framework\Testcase
  • PHPUnit\Framework\TestListener
  • PHPUnit\Framework\Test

に変わりました。

PHPUnit\Framework\Testcase に関しては PHPUnit_Framework_Testcase との共存期間(両方使える期間)があったのですが、他のクラスは何故か共存期間がありませんでした。何故なのだろうか。

mt_rand と mt_srand

PHPには mt_rand という乱数を発生させる関数がありますが、実はこれはバグっていました。

kusano-k.hatenablog.com

このバグが PHP 7.1 で修正されました。

https://www.php.net/manual/ja/function.mt-rand.php

mt_srand ("s" rand です)は、 mt_rand で使う乱数のseedを固定し、乱数に再現性をもたせることができるメソッドです。*1 これで乱数を固定させてテストしているコードがあり、PHPのバージョンを上げたことで、そのテストが落ちるようになりました。*2

mt_rand は、第二引数に MT_RAND_PHP を指定すると、これまでの間違った挙動を維持できるようになっています。こういうところがPHPっぽいですね。*3

https://www.php.net/manual/ja/migration71.incompatible.php#migration71.incompatible.fixes-to-mt_rand-algorithm

グローバル変数の扱い

PHPUnit 5 では、グローバル変数はテストケースごとにリセットされていました。PHPUnit 6 からはリセットされなくなりました。

古くて凶悪なテストの中には $_GET などのグローバル変数を直接弄っているテストがあり、他のテストに影響を及ぼしていることがありました。

これがなかなか難題で、単体でテスト回すと通るけど、通しで回すと落ちるのです。依存関係もよくわからず修正が困難を極めることもありました。

@backupGlobals とか --strict-global-state で以前の仕様を維持できますが、テストが激しく遅くなるのであまりおすすめしません。修正方法がわかんなかったら仕方ないですけどね...。

non-numeric test phpunit6 ochiru

stringとintの比較をしているコードがあり、問題なく動くのですが、PHPUnit 6 で動かすと non numeric な値と比較するのヤメレ!とか言われてテストが落ちます。

<?php

// $request_time は string 型になる
$request_time = $_GET['request_time'];

// $current_time と TOKEN_EXPIRE_SEC は int
// PHPUnitだと何故かここでエラーになる
if ($request_time > $current_time + TOKEN_EXPIRE_SEC) {
    ....
}

多分 error_reporting の設定の違いが原因ですが、何故落ちているかよくわからなくて、修正終盤で疲れていた僕のプルリクタイトルがこちらです。

f:id:yoshiki_utakata:20191203184432p:plain

アサーションをしていないテスト

「Exceptionが発生しなければOK」としてあるテストがいくつかあり、そのテストの中では assertSame のようなアサーションが一切ありませんでした。

PHPUnit 6 から、アサーションの無いテストは「アサーション忘れてるぞ!」と言われ、落ちるようになりました。

アサーションが不要な場合はDocコメントに @doesNotPerformAssertions を記入しておきましょう。

アサーションがほんとに忘れられている場合もありました!何故かifで結果を判定しているのもありました!!アサーションをつかってください!!!

PHPUnit 6 にしてよかったこと

アサーション忘れに気づくようになり、その他の面でもかなり使いやすくなっているのですが、一番の恩恵は高速化です。

PHP 5.6/PHPUnit 5 -> PHP 7.2/PHPUnit 6 にするだけでテストの時間が20分から4分程度になりました。3台あったCI環境ですが、1台ですむようになりました。

PHP7による高速化や、PHPUnit6のglobal stateなどによる高速化があったんだと思います。突然5倍の速さになりました。ユニットテストって純粋なPHPの速度依存なので、バージョン上げるだけでここまでの劇的改善となりました。PHP7が早いというのは本当だった。*4

そして時代は

PHPUnit 6 はEOLを迎えているので、すぐに PHPUnit 7 にしなきゃいけないんですけどね。。。

*1:"s"は多分seedのs

*2:正確には、これはPHPUnitではなく、PHPのバージョンアップによるものなのですが、PHPUnitのバージョンを上げる時に初めて気づいたのでここで書いちゃってます。

*3:他にも、implodeは引数の順番を入れ替えても正常に動作するなど、PHPには互換性維持のための面白い仕様がいくつかあります。 https://www.php.net/manual/ja/function.implode.php

*4:本番アプリケーションにPHP7を適用した際は、微妙に早くなったのですが、そこまで劇的な改善ではなく。。。これは結局、PHPの処理時間よりも、APIのやり取りとかのネットワーク部分だったり、PHP以外の部分の方が、レスポンス時間に占める割合が多かったからなんですよね。一方ユニットテストは純粋にPHPの速さですからね。