PHPではDB(データベース)を扱うことが多いです(少なくとも自分の関わる案件では多いです)。DBを扱う時よくあるDB定義のパターンに一塊のデータを正規化して複数のテーブルに分割するパターンがあります。例えば、次の様な定義になります。
一つのスレッドにn個のレスがついている形です。トランザクション抜き(外部キー制約も抜き)にこれを操作する場合、データの不整合で問題が起こる場合があります。例えば、スレッドの削除をしたい時、スレッドの削除と共にスレッドに紐づくレスの削除をします。この処理の最中にDBが止まると復帰時にスレッドのないレスレコードが現れます。トランザクションを適切な範囲(例ならばスレッドの削除から子レス全ての削除まで)にかけることでこれを防げます。
DB上のデータの整合性を守るためにトランザクションは欠かせない仕組みです。必然クリティカルな部分(お金や商品のやり取りなど。お金を払っただけ、商品を送っただけとか起きるとえらいことに……)はトランザクションで守られる場合が多いです。クリティカルな部分ということは動作の確認の必要も重大です。トランザクションに失敗した場合、適切にロールバックされることを任意にテストしたいものです。この記事ではその方法の一つを紹介します。
コードは次です。実務のコードの改造なので保存処理になっています。
public function testSaveRollBack(): void { // 変更対象のスレッドを取得 /** @var Thread $thread */ $thread = Thread::inRandomOrder()->first(); // スレッドの名前を書き換え $originalName = $thread->name; $editedName = Str::random(12); $thread->name = $editedName; // threadに紐づいたresponseのsaveが失敗するようにモックに差し替え // Laravelのシングルトン機能を元にLaravel側でモックResponseをResponseとしてnewしてもらう $this->app->singleton(Response::class, static function () { return Mockery::mock(Response::class, static function ($mock) { // このモックはsaveメソッドを呼ばれた時、必ずfalseをreturnする /* @var Mockery\Mock $mock */ $mock->shouldReceive('save') ->andReturnFalse(); }); }); // モックのResponseインスタンスにスレッドの子を置き換え。 // 可視性でがっちり守っている相手ならリフレクションを使う。 // DIの仕組み付きならそちらに従う。 $thread->response = app()->make(Response::class); // responseを巻き込んだスレッドのトランザクション付きsaveを実行. 保存処理が失敗を返すことを確認 $result = (new ThreadRepository())->save($thread); $this->assertFalse($result); // 同一のスレッドをDBから再度取得 $editedThread = Thread::find($thread->id); // 取得したスレッドの名前が変更前の名前と同一か確認 $this->assertEquals($originalName, $editedThread->name); }
Mockeryにより対象のクラスのメソッドの返り値(ここでは保存処理)を変えたり、例外を投げさせたりできます。トランザクション処理中にそのような失敗を起こすことでトランザクション処理失敗のロールバックを動作させます。assertではトランザクション処理に失敗したことを示す返り値とロールバックされているはずのDBを調べます。
mockery/mockery: Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succinct API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL).
Mockery:Mockery1.0 日本語で読みやすいが記事作成時点の最新は1.3なので注意