こんにちはみなさん
Webでデータを更新したら、そのログを取っておくという処理をコントローラに一緒に書くのは、まあ、よくあることなのです。しかし、システムが肥大化していき、「ユーザにメール出さなきゃ」とか、「並び順の変更もしなきゃ」とか、いろんな事が出てくると、コントローラが煩雑になっていきます。しかも、其のデータ更新が別の箇所で発生していたら、また同じような処理を書かなければならない。。。
LaravelのEloquentを使っている場合、データ更新という中心的な処理に対し、ログを取ったりメールを出したりといった「脇道の処理」はイベントを使うのが楽ちんです。
楽ちんなんですが、テストでfactoryを使ってデータを作ると、いちいちメールが飛んだり、ログが作られたりしますので、これはこれで面倒なのです。
というわけで、factoryでデータ作るときはイベントリスナーを実行しないようにするやり方をご紹介しましょう。
ちなみに、Laravelは7を使っています。
3行
- factoryのときもイベントは発生する
- fakeForを使えば、一時的にイベントの発生を抑制できる
- ModelのeventDispatcherを変える技もあるよ
問題設定
メモを更新するwebの処理についてのテストを書いてみて、イベントリスナーの挙動を見てみます。
処理の作成
「メモは更新すると、その履歴を保存する」という、簡単な所作をイベントを使って定義します。
<?php
namespace App\Models;
use App\Events\MemoUpdating;
use Illuminate\Database\Eloquent\Model;
class Memo extends Model
{
protected $dispatchesEvents = [
'updating' => MemoUpdating::class,
];
public function logs()
{
return $this->hasMany(MemoLog::class);
}
}
ここで$dispatchesEvents
によって、発生するイベントを定義します。今回は更新時のことしか考えていないので、updating
のみ定義しています。
諸々省略して、リスナーを見てみます。
<?php
namespace App\Listeners;
use App\Events\MemoUpdating;
class WriteMemoLog
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param MemoUpdating $event
* @return void
*/
public function handle(MemoUpdating $event)
{
$memo = $event->memo;
$original_content = $memo->getOriginal('content');
$memo->logs()->create([
'content' => $original_content
]);
}
}
コントローラはデータの更新だけします。
public function update(Request $request, Memo $memo)
{
$memo->content = $request->input('content');
$memo->save();
return $memo;
}
他色々やってたりしますが、まあ、何した以下はわかるのではないでしょうか。
テストを走らせる
とりあえず適当なテストを作りましょう。
/**
* @test
*/
public function メモのアップデート()
{
$memo = factory(Memo::class)->create();
$this->putJson(route('memo.update', $memo->id), [
'content' => 'あいうえお'
]);
// 変更している
$new_memo = Memo::find($memo->id);
$this->assertEquals('あいうえお', $new_memo->content);
// ログがひとつできている
$this->assertEquals(1, MemoLog::count());
$log = MemoLog::first();
$this->assertEquals($memo->content, $log->content);
}
目論見通りですね。
factoryデータを更新
ところが、factoryで作ったデータを更新しようとすると、こんなことになります。
/**
* @test
*/
public function factoryのデータを事前更新()
{
$memo = factory(Memo::class)->create();
$memo->content = 'アイウエオ';
$memo->save();
$this->putJson(route('memo.update', $memo->id), [
'content' => 'あいうえお'
]);
$new_memo = Memo::find($memo->id);
$this->assertEquals('あいうえお', $new_memo->content);
$this->assertEquals(1, MemoLog::count());
}
これは実行すると以下のようにエラーが出ます。
> phpunit
PHPUnit 8.5.5 by Sebastian Bergmann and contributors.
...F 4 / 4 (100%)
Time: 896 ms, Memory: 26.00 MB
There was 1 failure:
1) Tests\Feature\MemoTest::factoryのデータを事前更新
Failed asserting that 2 matches expected 1.
/var/www/tests/Feature/MemoTest.php:49
FAILURES!
Tests: 4, Assertions: 7, Failures: 1.
Script phpunit handling the test event returned with error code 1
何が起こったかというと、factoryで作ったデータをちょっと変えてsaveしたところ、updatingイベントが走ってログが作成され、想定している個数とズレが発生したということです。
factoryのイベントを止める
まあ、factoryでイベントが発生しても、この程度であれば困らないっちゃ困らないのですが、とはいえ、意図しないデータが勝手に作成されてしまったりすると、テスト結果に変な不確実性が出来ちゃうかもしれないので、イベントの発生を止めることを考えてみましょう。
fakeForを使う
一番簡単なのはEvent::fakeFor
を使って、対象部分だけイベントリスナーの挙動を止める方法です。
/**
* @test
*/
public function イベントを止めたい()
{
// fakeForのなかのみイベントリスナーが作動しない
$memo = Event::fakeFor(function () {
$memo = factory(Memo::class)->create();
$memo->content = 'アイウエオ';
$memo->save();
return $memo;
});
$this->putJson(route('memo.update', $memo->id), [
'content' => 'あいうえお'
]);
$new_memo = Memo::find($memo->id);
$this->assertEquals('あいうえお', $new_memo->content);
$this->assertEquals(1, MemoLog::count());
}
これでテストは通ります。
一時的にModelのeventDispatcherを変える
復数のデータを扱っているときはfakeForが面倒なときもあります。
かなり裏技っぽいですが、factoryでデータを作っているときだけ、ModelのeventDispatcherを変更するという方法もあります。
/**
* @test
*/
public function イベントを止めたい()
{
// eventDispatcherを一時的に入れ替える
$original_event = Event::getFacadeRoot();
Model::setEventDispatcher(new EventFake($original_event, []));
$memo = factory(Memo::class)->create();
$memo->content = 'アイウエオ';
$memo->save();
Model::setEventDispatcher($original_event);
$this->putJson(route('memo.update', $memo->id), [
'content' => 'あいうえお'
]);
$new_memo = Memo::find($memo->id);
$this->assertEquals('あいうえお', $new_memo->content);
$this->assertEquals(1, MemoLog::count());
}
factoryでいちいちイベントが発生したかどうかを見る必要もなければ、このような方法でもありかなと思います。
まとめ
そんなわけで、テスト時のfactoryによるデータ作成時に、イベントリスナーを動作させない方法を紹介しました。
普通はfakeFor
を使うのでしょうが、複雑な系で復数のデータのfactoryを走らせたい場合は、eventDispacherの入替えを検討するのもありって感じです。
今回はこんなところです。