😊

PHPUnitを低速化する技術

2024/01/10に公開

この記事はPHP Advent Calendar 2023 24日目の記事です。

https://qiita.com/advent-calendar/2023/php

かつ、Lancers(ランサーズ) Advent Calendar 2023 の24日目の記事でもあります。

https://qiita.com/advent-calendar/2023/lancers

モチベーション

社内でテストが遅い原因を説明する機会がありました。
せっかくですので、社内ドキュメントに書いた内容をこの機会に公開させていただきます。

TL;DR

  • Attributesに@runTestsInSeparateProcessesを付けるとテストが遅くなる
  • エイリアス/インスタンスモックを使うと@runTestsInSeparateProcessesを使わざるを得ない状況が生まれる

Attributesに@runTestsInSeparateProcessesを付けるとテストが遅くなる

RunTestsInSeparateProcesses属性は、テストケースクラスのすべてのテストを別々のプロセス(テストごとに1つの別々のプロセス)で実行することを指定するために使用できます。

2. Attributes — PHPUnit 10.5 Manual

このAttributesを付けるとクラス内のテストケースの数だけPHPプロセスが起動するため、プロセス起動のオーバーヘッド分テストが遅くなります。

嫌でも@runTestsInSeparateProcessesを付けざるを得ないシチュエーションがある

なぜ、テストが遅くなる@runTestsInSeparateProcessesをわざわざ使う必要があるのでしょうか?

@runTestsInSeparateProcessesに主に名前の衝突を回避するために利用する機能です。

どのような場面で、テスト時に名前の衝突が発生するのか、モック(mockery)のしくみから説明していきます。

モック(mockery)のしくみ

モック(mockery)のしくみをかいつまんで説明すると下記になります。

  • mockeryはモック対象のクラスを継承した別名のクラスを生成する
    • FooをモックするとFooを継承したMockery_0_Foo を生成する
  • 生成したモッククラスでメソッドをオーバーライドすることでモックを実現する

詳細は下記の記事に詳しく載っています。

https://tech.yappli.io/entry/php_mockery_codereading

staticメソッドをモックしたい、あるいはモック対象クラスがnewされちゃっている時

ところで、staticメソッドをモックしたい時や、モック対象クラスがnewされちゃっている時は前述のしくみではモックできません。

staticメソッドの場合は、モックによってMockery_0_Foo を生成したとしてもソースコード上でFoo::baa() をコールすると親クラスのメソッドが呼ばれてしまいます。
同様に、new Foo()されている時はMockery_0_Fooを生成しても親クラスを参照しているためモックを差し込めません。

そこでmockery::mock(’alias:Foo’)もしくはmockery::mock(’overload:Foo’) を使います。
これにより、エイリアス/インスタンスモックが生成され、強い依存関係でもモックを差し込むことができます。

エイリアス/インスタンスモックを作ると名前が衝突する

この時、どこか別の場所でモックせずに親クラスを利用していた場合、親クラスであるFooと名前が衝突してエラーになります。

Mockery\Exception\RuntimeException: Could not load mock Foo, class already exists

@runTestsInSeparateProcessesを使うことでテストケース毎にPHPのプロセスが分離されるため名前の衝突を回避できるのです。

まとめ

本稿では、PHPUnitを低速化する技術として@runTestsInSeparateProcessesを紹介しました。
依存関係に無頓着でいるとエイリアス/インスタンスモックを使うことになり、最後は@runTestsInSeparateProcessesを使う羽目になるので注意しましょう。

ランサーズ株式会社

Discussion