📝
PHPUnit × Mockery の overload を使うとテストが不安定になる問題と対処法
PHPUnit と Mockery を使ってユニットテストを書いている中で、
Mockery の overload: を使用したテストが 実行順序によって落ちたり通ったりする という問題に遭遇。
ローカルでは通るのに CI で落ちる ―
理由はシンプルですが、知っておかないとハマるポイントだったのでまとめておきます。
結論
- Mockery の
overload:は クラス定義そのものをモックに置き換える - そのため 同じプロセスで複数回実行されるとエラーになる
- PHPUnit の実行順やプロセス分割の仕方によって成功・失敗が変わる
- ローカルと CI で挙動が変わる原因にもなる
- 基本的に overload は使わない方がよい。DI で差し替え可能な設計にした方が安定する
1. Mockery の overload とは?
通常のモック
$mock = Mockery::mock(SomeClass::class);
// 新しいモックオブジェクトを作るだけ
overload モック(危険枠)
$mock = Mockery::mock('overload:' . SomeClass::class);
// クラス定義そのものを書き換える
overload: は “差し替え” ではなく “上書き” を行う。
そのため正しく動けば強力だが、テスト環境とは非常に相性が悪い。
2. なぜ実行順序で落ちるのか
overload: の動作は次のとおり。
1. テストAで SomeClass がロードされる
2. Mockery がクラス定義そのものをモックに置き換える
3. 同じプロセスでテストBが続けて実行される
4. 再び overload を実行
5. 「クラスはすでに存在します」 → class already exists エラー
つまり、「同一プロセス内で2回以上 overload が走るとアウト」。
3. 実際に発生するパターン
成功パターン
TestA(overload なし)
TestB(overload あり)
--- プロセスが切り替わる ---
TestC(overload あり)
→ 問題なし
失敗パターン
TestA(overload あり)
TestB(overload あり)
→ 同じプロセスで連続実行 → エラー
4. なぜローカルは通るのに CI では落ちるのか?
これは、CI がテストを分割したり、並列実行したり、
内部で複数のプロセスを扱ったりするため 実行順が不安定になる のが原因。
たとえば:
-
ローカル
→ テスト実行順がほぼ固定で、overload が連続しないため問題が出にくい -
CI
→ テストを複数のグループに分けて実行する
→ グループの順序・プロセスの切り替わり方が変わる
→ たまたま overload テストが同じプロセスに固まる
→ 落ちる
“ローカル OK なのに CI だけ落ちる”
という典型的な不安定テストの原因になる。
5. 解決策
1. overload を使わない(最良)
DI(依存注入)を使ってテスト可能な設計にする。
class SomeService
{
public function __construct(
private SomeClass $client
) {}
}
テスト側は通常のモックを渡せばよい。
2. テストを別プロセスで実行する(応急処置)
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
3. テストクラス全体を別プロセスにする
/**
* @runTestsInSeparateProcesses
*/
class SomeServiceTest extends TestCase
ただし、これらはあくまで「モックを安全に使うための対症療法」。
6. まとめ
- Mockery の
overload:はクラス定義を破壊するのでテスト環境と相性が悪い - PHPUnit の実行順やプロセス構成が変わると、成功・失敗が変わってしまう
- ローカルと CI の挙動差を生む最大の原因になる
- 基本的には使わず、DI で差し替える設計にするのがベスト
7. 個人的な学び
- テストは「プロセスに依存した瞬間に不安定化する」
- “クラス定義を書き換えるモック” は長期的に必ずトラブルになる
- シンプルな DI 設計が一番テストしやすく、環境差異も出ない
- ローカル成功=安全 ではない。CI で崩れる余地を常に考える
Discussion