Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Testing issue with Event::fake() on Eloquent models #18066

Closed
davidianbonner opened this issue Feb 23, 2017 · 23 comments
Closed

Testing issue with Event::fake() on Eloquent models #18066

davidianbonner opened this issue Feb 23, 2017 · 23 comments

Comments

@davidianbonner
Copy link

davidianbonner commented Feb 23, 2017

  • Laravel Version: 5.4
  • PHP Version: 7.1

Description:

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:

  • Adding Event::fake() to the createApplication method before returning the $app; or
  • Using the example below, you can swap in the faked dispatcher by adding Model::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?

Steps To Reproduce:

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'      => 'foo@bar.com',
            'password'   => bcrypt('secret'),
        ]);
       
        // This will fail
        Event::assertDispatched(\App\Users\Events\UserCreated::class, function ($e) use ($user) {
            return $e->user->id === $user->id;
        });
@davidianbonner
Copy link
Author

davidianbonner commented Feb 23, 2017

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());

@spirant
Copy link

spirant commented Mar 3, 2017

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\src\Illuminate\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');
    }
}

@davidianbonner
Copy link
Author

davidianbonner commented Mar 3, 2017

@spirant what happens when you use the $events property on the model instead of the static methods?

    protected $events = [
        'created' => AlcieAddedEvent::class,
    ];

@spirant
Copy link

spirant commented Mar 3, 2017

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'];
}

@davidianbonner
Copy link
Author

@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;
});

@spirant
Copy link

spirant commented Mar 6, 2017

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!

@joshbrw
Copy link

joshbrw commented Jul 18, 2017

@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)

@themsaid
Copy link
Member

@joshbrw if you use Event::fake all your listeners won't be triggered and thus the effect you describe.

@joshbrw
Copy link

joshbrw commented Jul 18, 2017

@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!

@davidianbonner
Copy link
Author

@joshbrw can you provide your test case?

Have you tried adding Model::setEventDispatcher(Event::getFacadeRoot()); after Event::fake()?

@joshbrw
Copy link

joshbrw commented Jul 18, 2017

@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

@chargoy
Copy link

chargoy commented Nov 7, 2017

@davidianbonner Hi!
I tried successfully this hacky solution:

$initialDispatcher = Event::getFacadeRoot();
Event::fake();
Model::setEventDispatcher($initialDispatcher);

#19952

@seivad
Copy link

seivad commented Aug 15, 2018

@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...

@X-Coder264
Copy link
Contributor

This has been fixed in #25185 which has been released in 5.6.34.

@guice
Copy link

guice commented Nov 15, 2018

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).

@X-Coder264
Copy link
Contributor

@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?

@guice
Copy link

guice commented Nov 16, 2018

@X-Coder264 These lines:

Event::fake(NonImportantEvent::class);
Post::observe([PostObserver::class]);

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); 

@X-Coder264
Copy link
Contributor

@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.

@sevillaarvin
Copy link

@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);
    }

@laravel laravel deleted a comment from majus28 Aug 1, 2019
@jartaud
Copy link

jartaud commented Nov 21, 2019

@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');
        ...
    }

@jeromefitzpatrick
Copy link

To target model events, this works:

Event::fake([ 'eloquent.creating: ' . \App\Models\User::class, ]);

@damms005
Copy link

damms005 commented Feb 4, 2022

Event::fake([ 'eloquent.creating: ' . \App\Models\User::class, ]);

@jeromefitzpatrick Thanks for this information.

However, I want to ask: please which part of the framework did you find that this is the construct of the name of the event dispatched? i.e. where you got the string 'eloquent.creating: ' . \App\Models\User::class?

I have checked the documentation and also Illuminate\Database\Eloquent\Concerns\HasEvents. I cannot find anything.

I need it because I want to comment a part of my code with a link to how I got this. I would have loved to use the link to your comment as the "proof". Bu then some future version of Laravel may change this and I want to be able to follow such changes.

Thanks

@X-Coder264
Copy link
Contributor

X-Coder264 commented Feb 4, 2022

@damms005 The logic for that is in HasEvents class -> https://github.com/laravel/framework/blob/v8.82.0/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php#L169-L191

static::$dispatcher->{$method}(
            "eloquent.{$event}: ".static::class, $this
        )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests