現象
Laravel 10のテスト実行時に「There is no active transaction」というエラーが発生した。一般的によく言われている原因とされるtruncateなどのデータベース操作は行っていない状態で発生しており、原因の特定が難しかった。
特徴として:
- テストを単体で実行すると問題なく終了するが、一連のテストを連続実行すると発生する
- データプロバイダー(dataProvider)を使用したテストで発生する傾向がある
- PhpSpreadsheetを使った処理を含むテストで発生することがわかった
- ログ調査の結果、トランザクションは正しく開始され(レベル1)、コミット直前まで維持されていることが確認できた
エラーメッセージからは、アクティブなトランザクションがない状態でトランザクション操作(主にロールバックやコミット)を実行しようとしていることがわかるが、トランザクションの開始と終了が適切に行われているにも関わらずエラーが発生するという矛盾した状況だった。
原因:特定困難も関連要素を特定
「There is no active transaction」エラーの正確な原因特定は困難でしたが、以下の要素が関連していることが判明しました:
-
テスト間での状態共有
- 複数テストの連続実行時のみエラー発生
- データプロバイダー使用時に特に発生
-
リソース集約的な処理の影響
- PhpSpreadsheetを使った処理を含むテストで発生
- メモリ管理がトランザクション状態に影響の可能性
-
結合テストの複雑性
- 複数コンポーネント(ファイル操作、DB操作等)の相互作用
- テスト粒度が大きすぎることによる問題特定の難しさ
単一の明確な原因は特定できず、「環境依存の問題」の典型例と言えます。
反省点:テスト設計
-
テスト粒度
- 各クラス単位の単体テストをより充実させるべき
- 大きな結合テストは問題発生時の原因特定が困難に
-
テスト独立性
- テスト間で状態共有がないよう設計すべき
- 特にDB操作やファイル操作を含むテストでは重要
解決策:PHPUnitのプロセス分離設定を適切に活用
今回の「There is no active transaction」エラーに対する解決策は、PHPUnitのプロセス分離機能を適切に設定することでした。
1. テストクラスにアノテーションを追加
問題が発生するテストクラスに以下のアノテーションを追加しました:
/**
* @runTestsInSeparateProcesses
*/
class FileImportTest extends TestCase
{
// テストコード
}
これにより、このテストクラスの各テストメソッドが独立したPHPプロセスで実行されるようになり、テスト間での状態共有が防止されました。
2. プロセス分離の選択的適用
テスト全体に対してphpunit.xml
でプロセス分離を有効にすることも可能ですが、以下のような新たな問題が発生する可能性があります:
<!-- 全テストでプロセス分離を有効にする設定 -->
<phpunit processIsolation="true">
この設定を行うと、今度は以下のようなシリアライズエラーが発生することがありました:
An error occurred inside PHPUnit.
Message: Serialization of 'Illuminate\Http\UploadedFile' is not allowed
これは、テスト間でファイルアップロード機能を使用している場合に特に発生しがちです。
3. 最適な解決策
最終的には、以下のアプローチを採用しました:
- グローバルな
processIsolation
設定は無効(デフォルト)のままにする - リソース集約的な処理や状態共有の問題が発生しやすいテストクラスにのみ
@runTestsInSeparateProcesses
アノテーションを追加する - それ以外のテストは通常通りのプロセスで実行し、パフォーマンスを維持する
この方法により、問題のあるテストだけを分離して実行することができ、エラーを解消しつつもテスト全体の実行速度への影響を最小限に抑えることができました。