I've been migrating some model events over to the new the object oriented $events
array and noticed that any associated tests are failing using the new Event mocking helpers.
After some poking around, it appears the Model dispatches on Illuminate\Events\Dispatcher
instead of Illuminate\Support\Testing\Fakes\EventFake
is it set in the boot method of the DatabaseServiceProvider
.
I've found two ways to correct this:
Event::fake()
to the createApplication
method before returning the $app; orModel::setEventDispatcher(Event::getFacadeRoot());
after Event::fake()
.Event::fake();
Model::setEventDispatcher(Event::getFacadeRoot());
Perhaps one of the above need be mentioned in the documentation? Though, from the point of view that Event::fake()
should make testing events easier – should instances of the event dispatcher be replaced or is was this the intended behaviour?
A simple example:
<?php
namespace App\Users;
use App\Users\Events\UserCreated;
use App\Users\Events\UserUpdated;
class User extends Authenticatable
{
/**
* The event map for the model.
*
* @var array
*/
protected $events = [
'created' => UserCreated::class,
'updated' => UserUpdated::class,
];
}
/** @test */
function user_created_event_is_triggered()
{
Event::fake();
$user = User::create([
'first_name' => 'foo',
'last_name' => 'foo',
'email' => '[email protected]',
'password' => bcrypt('secret'),
]);
// This will fail
Event::assertDispatched(\App\Users\Events\UserCreated::class, function ($e) use ($user) {
return $e->user->id === $user->id;
});
The above has been updated, adding Event::fake()
... within createApplication
causes issues with all events registered in EventServiceProvider
. Swapping the instance with fake()
forgets all previously registered events. Looks like the second option (highlighted below) is the only way to test model events at the moment.
Event::fake();
Model::setEventDispatcher(Event::getFacadeRoot());
I have a similar issue where Event::fake(); is stopping events triggering but the Event::assertDispatched is not being triggered even adding the code suggested above. When I remove the Event::fake() then the events are dispatched and broadcasting when phpunit is run, so I am sure that the events are triggering correctly.
Pusher output from running phpunit with Events not being faked:
API MESSAGE
‌
Channel: private-Alcie, Event: App\Components\Alcie\Events\AlcieAddedEvent
17:30:34
{
"alcie": {
"alcie_id": 272,
"parent_id": 1,
"alcie_type": "Test",
"last_update_by": "unknown",
"last_update": "2017-03-03 17:35:11"
}
}
Code below:
````
1) Tests\Feature\AlcieApiTest::testUpdate
The expected [App\Components\Alcie\Events\AlcieAddedEvent] event was not dispatched.
Failed asserting that false is true.
C:\Users\Tim\Code\Homestead\pcms4-1\vendor\laravel\framework\srcIlluminate\Support\Testing\Fakes\EventFake.php:29
C:\Users\Tim\Code\Homestead\pcms4-1\vendor\laravel\framework\src\Illuminate\Support\Facades\Facade.php:221
C:\Users\Tim\Code\Homestead\pcms4-1\tests\Feature\AlcieApiTest.php:364
C:\Users\Tim\Code\Homestead\pcms4-1\tests\Feature\AlcieApiTest.php:364
````
````php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Support\Facades\Event;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\Components\Alcie\Alcie;
// Events
use App\Components\Alcie\Events\AlcieAddedEvent as AlcieAddedEvent
class AlcieApiTest extends TestCase
{
// Disable middleware
use WithoutMiddleware;
use DatabaseTransactions;
public function testUpdate(){
Event::fake();
Model::setEventDispatcher(Event::getFacadeRoot());
$alcie = Alcie::create(['alcie_type' => 'Test', 'parent_id' => 1]);
Event::assertDispatched(AlcieAddedEvent::class, function ($e) use ($alcie) {
return $e === $alcie;
});
}
}
````
````php
namespace App\Components\Alcie;
use Illuminate\Database\Eloquent\Model;
// Events
use App\Components\Alcie\Events\AlcieAddedEvent;
class Alcie extends Model
{
public $timestamps = false;
protected $primaryKey = 'alcie_id';
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'tblALCIE';
protected $fillable = ['last_update', 'alcie_type', 'description', 'last_update_by', 'parent_id'];
public static function boot()
{
parent::boot();
static::created(function($model)
{
event(new AlcieAddedEvent($model));
});
}
}
````
````php
namespace App\Components\Alcie\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class AlcieAddedEvent implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
public $alcie;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($alcie)
{
$this->alcie = $alcie;
}
/**
* Get the channels the event should broadcast on.
*
* @return Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('Alcie');
}
}
````
@spirant what happens when you use the $events
property on the model instead of the static methods?
protected $events = [
'created' => AlcieAddedEvent::class,
];
Hi @dbonner1987. Thanks for the quick response. I have removed the static function boot() and replaced it with the protected $events property you have suggested - which is more elegant anyway, so I may continue to use this where possible ;-). However, I am still getting the same failure on phpunit.
````php
namespace App\Components\Alcie;
use Illuminate\Database\Eloquent\Model;
// Events
use App\Components\Alcie\Events\AlcieAddedEvent;
use App\Components\Alcie\Events\AlcieUpdatedEvent;
class Alcie extends Model
{
protected $events = [
'created' => AlcieAddedEvent::class,
];
public $timestamps = false;
protected $primaryKey = 'alcie_id';
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'tblALCIE';
protected $fillable = ['last_update', 'alcie_type', 'description', 'last_update_by', 'parent_id'];
}
````
@spirant it does make your models a lot cleaner.
The issue is actually in your callback that you pass to assertDispatched()
. The argument provided by the callback in an instance of AlcieAddedEvent
where as the inherited $alcie variable is actually an instance of your Aclie
. The callback will be returning false.
You need to use the $alcie property ($e->alcie
) you assign in the event constructor to test the event works correctly.
Event::assertDispatched(AlcieAddedEvent::class, function ($e) use ($alcie) {
return $e->alcie === $alcie;
});
Thank you for that. You are correct. I had tried many different versions of the return statement previously. However that was before finding this issue and the suggested Model::setEventDispatcher(Event::getFacadeRoot());
fix. I should have tried rewriting that section again once I had added your fix, but the error message led me a little astray when it said that the event was not dispatched rather than the result returned was not passing the comparison test.
Thank you once again for helping me sort this!
@themsaid any reason for closing this without a resolution? I have a trait on my models that hooks into static::creating(
to set a UUID as the ID instead of primary key, but when using Event::fake
in my tests it removes the registered events, meaning it's attempting to create my models without a key (due to getIncrementing()
returning false
in my trait for UUID reasons)
@joshbrw if you use Event::fake all your listeners won't be triggered and thus the effect you describe.
@themsaid Yeah I get that. I guess that's the intended functionality? I just want to be able to assert some code is firing an event, but I guess I'll have to leave it. Thanks anyway!
@joshbrw can you provide your test case?
Have you tried adding Model::setEventDispatcher(Event::getFacadeRoot());
after Event::fake()
?
@dbonner1987 yeah I tried that to no avail. I was just doing factory(User::class)->create();
and it was complaining about the id
missing as the trait wasn't setting it. The trait is from this package; https://github.com/alsofronie/eloquent-uuid/blob/master/src/UuidModelTrait.php
@davidianbonner Hi!
I tried successfully this hacky solution:
$initialDispatcher = Event::getFacadeRoot();
Event::fake();
Model::setEventDispatcher($initialDispatcher);
@chargoy answer is the only way I was able to perform tests in L5.6 for dispatched events. It used to work in older versions of Laravel in a different way.
This needs to be fixed, it's silly to disable the database insert events when really we are generally testing for custom events such as image processing, send an email, notify additional platforms via Guzzle, etc...
This has been fixed in https://github.com/laravel/framework/pull/25185 which has been released in 5.6.34.
I literally just had to implement @chargoy's hack solution the other day to get Event trigger tests working for an entity model. I'm using an Observer via Model::observe(). Did something regress? Using Laravel ^5.7.x (updating often).
@guice The test which confirms that event dispatching works properly is still there and it's passing -> https://github.com/laravel/framework/blob/5.7/tests/Integration/Events/EventFakeTest.php
Can you please take a look at it and see what are you doing differently so that the event doesn't get properly dispatched and executed?
@X-Coder264 These lines: https://github.com/laravel/framework/blob/f88917adc292e7e2960e9336a0d89206b41155fe/tests/Integration/Events/EventFakeTest.php#L65-L66
You're attaching the ::observe()
after calling ::fake()
. We're calling ::observe before ::fake is called. What happens if you flip line 64 and 65?
Note, order of operation:
$initialDispatcher = Event::getFacadeRoot();
Event::fake();
Model::setEventDispatcher($initialDispatcher);
Observer is attached in AppServiceProvider::boot()
, before Event::fake()
is called.
What happens if you flip the lines:
Post::observe([PostObserver::class]);
Event::fake(NonImportantEvent::class);
@guice I'll take a look at it and check what's going on sometime in the next couple of days when I'll have some free time.
@davidianbonner Hi!
I tried successfully this hacky solution:$initialDispatcher = Event::getFacadeRoot(); Event::fake(); Model::setEventDispatcher($initialDispatcher);
19952
I'm using Laravel 5.8.29
and I still need to add these lines inside my test function. What did I miss? I have an observer class inside my AppServiceProvider boot function
public function boot()
{
Model::observe(ModelObserver::class);
}
@sevillaarvin I'm on L6 and I was getting: Integrity constraint violation: 19 NOT NULL constraint failed: receipts.uuid
when I used Event::fake. The key is to use Event::fakeFor like that:
```
// Model
/**
* Boot the model.
*/
public static function boot()
{
parent::boot();
static::creating(function ($receipt) {
$receipt->uuid = \Str::uuid();
});
}
// Test
public function test_receipt_can_be_created()
{
....
$response = Event::fakeFor(function () use($user) {
$response = $this->actingAs($user)->json('POST', route('receipts.store'), [
...
]);
Event::assertDispatched(ReceiptCreated::class);
return $response;
}, [ReceiptCreated::class]);
$response->assertRedirect(route('receipts.list', ['order' => 'desc']))
->assertSessionHas('success');
...
}
To target model events, this works:
Event::fake([
'eloquent.creating: ' . \App\Models\User::class,
]);
Most helpful comment
@davidianbonner Hi!
I tried successfully this hacky solution:
https://github.com/laravel/framework/issues/19952