Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tests for Event Listeners sendin email

I'm quite new to testing so any help would be appreciated. First things first, this is my code inside App directory (Laravel 5.5)

// controller
public function store(Request $request)
{    
        $foo = new Foo($request->only([
            'email', 
            'value 2', 
            'value 3',
        ]));
        $foo->save();

        event(new FooCreated($foo));

        return redirect()->route('/');
}

// Events/FooCreated
use App\Foo;

class FooCreated
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $foo;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Foo $foo)
    {
        $this->foo = $foo;
    }
}


// Listeners/

use App\Events\FooCreated;
use App\Mail\FooSendingEmail;

class EmailSendListener
{

    /**
     * Handle the event.
     *
     * @param  EnquiryWasCreated  $event
     * @return void
     */
    public function handle(FooCreated $event)
    {
        \Mail::to($event->foo->email)->send(new FooSendingEmail($event->foo));
    }
}

Now, I'm trying to write some tests for the event and the listener that triggers the email to sent, so I have created a method in unit/ExampleTest.php

namespace Tests\Unit;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

use App\Foo;
use Illuminate\Support\Facades\Event;
use App\Events\FooCreated;

class ExampleTest extends TestCase
{
    use RefreshDatabase;
    public function testEventTriggered(){
        Event::fake();

        $foo = factory(\App\Foo::class)->create();

        Event::assertDispatched(FooCreated::class, function($event) use ($foo){
             return $event->foo->id == $foo->id;
        });
    }
}

// assume similar this applies for emails according to docs https://laravel.com/docs/5.5/mocking#mail-fake

but when I run this test, fails with error The expected [App\Events\FooEvent] event was not dispatched. Failed asserting that false is true.

How can I fix the testfor events to pass and also to test sending email ?

Thanks in advance.

UPDATE

I have managed to add a test for firing the event on controller, but I 'd need a test for checking the emails are triggered

public function testStore()
{
        Event::fake();

        $this->post(route('foo.store'), [
            'full_name' => 'John Doe',
            'email'     => '[email protected]',
            'body'      => 'Lorem ipsum dolor sit amet',
        ]);

        $foo = Foo::first();

        Event::assertDispatched(FooCreated::class, function ($event) use ($foo) {
            return $event->foo->id === $foo->id;
        });
}


public function testEmailSent()
{
        Mail::fake();
        // similar to prevous one in order to fire the event
        $this->post(route('foo.store'), [
            'full_name' => 'John Doe',
            'email'     => '[email protected]',
            'body'      => 'Lorem ipsum dolor sit amet',
            'reference_code' => str_random(25),
        ]);

        $foo = Foo::first();

        Mail::assertSent(FooSendingEmail::class, function ($mail) use ($foo) {
            return $mail->hasTo($foo->email);
        });
}
like image 958
ltdev Avatar asked Jan 02 '23 21:01

ltdev


2 Answers

As mentioned in the comments, my advice would be to write one test for the controller, and another for the event listener. Since ultimately you don't know if that event may be removed from that controller in future, it makes sense to test the controller and listener classes in isolation.

Here's how I would test these classes:

Testing the controller method

The controller method does three things:

  • Return a response
  • Create a model instance
  • Fire an event

We therefore need to pass it all its external dependencies and check it carries out the required actions.

First we fake the event facade:

Event::fake();

Next step is to create an instance of Illuminate\Http\Request to represent the HTTP request passed to the controller:

$request = Request::create('/store', 'POST',[
    'foo' => 'bar'
]);

If you're using a custom form request class, you should instantiate that in exactly the same way.

Then, instantiate the controller, and call the method, passing it the request object:

$controller = new MyController();
$response = $controller->store($request);

It makes sense to then test the response from the controller. You can test the status code like this:

$this->assertEquals(302, $response->getStatusCode());

You may also want to check the content of the response matches what you expect to see.

You will then want to retrieve the newly created model instance, and verify it exists:

$foo = Foo::where('foo', 'bar')->first();
$this->assertNotNull($foo);

You can also use assertEquals() to check the attributes on the model if appropriate. Finally, you check the event was fired:

Event::assertDispatched(FooWasCreated::class, function ($event) use ($foo) { 
    return $event->foo->id === $foo->id; 
});

This test should not concern itself with any functionality triggered by the event, only that the event gets triggered.

Testing the event listener

Since all the event listener does is send an email when passed an event, we should test that it calls the Mail facade with the correct arguments. The first step is to fake the mail facade:

Mail::fake();

Then, create an instance of your model - you can use Eloquent but it's usually more convenient to use a factory:

$foo = factory(Foo::class)->create([]);

Then, trigger your event:

event(new FooCreated($foo));

Finally, assert the mail facade received a request with the appropriate arguments:

Mail::assertSent(MyEmail::class, function ($mail) use ($foo) { 
    return $mail->foo->id == $foo->id; 
});

Technically, these don't quite qualify as being unit tests because they hit the database, but they should cover the controller and event adequately.

To make them true unit tests, you'd need to implement the repository pattern for the database queries rather than using Eloquent directly and mock the repository, so you can assert that the mocked repository receive the right data and have it return a mock of the model. Mockery would be useful for this.

like image 64
Matthew Daly Avatar answered Jan 05 '23 15:01

Matthew Daly


You arent firing the event FooEvent its FooCreated and you are not calling the method on the controller that actually dispatches the event (at least not in the code you are showing).

// controller
public function store(Request $request)
 {    
    $foo = Foo::create($request->only([
        'email', 
        'value 2', 
        'value 3',
    ]));

    return redirect()->route('/');
}


// Foo model
public static function create(array $attributes = [])
{
    $model = parent::create($attributes);

    event(new FooCreated($foo));

    return $model;

}

///test
public function testEventTriggered()
{
    $foo = factory(\App\Foo::class)->create();

    Event::fake();

    Event::assertDispatched(FooCreated::class, function($event) use ($foo){
         return $event->foo->id == $foo->id;
    });

}

Or add a new method if you don't want to fire your event all the time:

// Foo model
public static function createWithEvent(array $attributes = [])
{
    $model = parent::create($attributes);

    event(new FooCreated($foo));

    return $model;

}
like image 22
Mike Miller Avatar answered Jan 05 '23 16:01

Mike Miller