事件监听器的测试发送电子邮件

4

我对测试很新,所以希望能得到帮助。首先,这是我的代码,位于App目录下(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));
    }
}

现在,我正在尝试为触发电子邮件发送的事件和侦听器编写一些测试,因此我在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

但是当我运行这个测试时,会出现错误:The expected [App\Events\FooEvent] event was not dispatched. Failed asserting that false is true. 我该如何修复事件测试才能通过,并且测试发送电子邮件?
提前感谢您的帮助。
更新:
我已经成功添加了一个控制器触发事件的测试,但我需要一个测试来检查是否触发了电子邮件。
public function testStore()
{
        Event::fake();

        $this->post(route('foo.store'), [
            'full_name' => 'John Doe',
            'email'     => 'johndoe@example.com',
            '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'     => 'johndoe@example.com',
            '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);
        });
}

但是错误仍然存在。 - ltdev
该事件位于控制器方法store()中,而不是在Foo::create()中,因此基本上我会在数据库中插入新记录,然后触发事件以发送电子邮件。 - ltdev
我已经更新了代码以进行审查。基本上,我与事件测试做了相同的事情,我访问控制器方法以触发事件,但是测试失败并显示错误“未发送可邮寄项。断言失败:false不为true。” - ltdev
@Lykos 不,当你测试事件的功能时,你需要在测试中触发它 - 不要调用控制器。你想要在隔离环境中测试事件。 - Matthew Daly
让我们在聊天中继续这个讨论 - ltdev
显示剩余10条评论
2个回答

9
如评论中所述,我的建议是为控制器编写一个测试,为事件监听器编写另一个测试。由于最终您不知道该事件是否会在将来从该控制器中删除,因此单独测试控制器和监听器类是有意义的。
以下是我如何测试这些类:

测试控制器方法

控制器方法有三个作用:

  • 返回响应
  • 创建模型实例
  • 触发事件

因此,我们需要传递其所有外部依赖项并检查它执行所需的操作。

首先,我们伪造事件门面:

Event::fake();

下一步是创建一个Illuminate\Http\Request实例来代表传递给控制器的HTTP请求:
$request = Request::create('/store', 'POST',[
    'foo' => 'bar'
]);

如果您正在使用自定义表单请求类,那么您应该以完全相同的方式实例化它。
然后,实例化控制器并调用该方法,将请求对象传递给它:
$controller = new MyController();
$response = $controller->store($request);

接下来测试控制器的响应是有意义的。你可以像这样测试状态码:

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

您可能还想检查响应内容是否与您期望看到的相匹配。

然后,您将希望检索新创建的模型实例,并验证其存在:

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

如果合适的话,您还可以使用assertEquals()来检查模型上的属性。最后,您可以检查事件是否被触发:

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

这个测试不应该关注由事件触发的任何功能,只需要确保事件被触发。

测试事件监听器

由于事件监听器只在传递事件时发送电子邮件,因此我们应该测试它是否使用正确的参数调用了Mail门面。第一步是伪造邮件门面:

Mail::fake();

接下来,创建一个模型实例 - 您可以使用 Eloquent,但通常更方便使用工厂:

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

然后触发你的事件:

event(new FooCreated($foo));

最后,断言邮件门面收到了带有适当参数的请求:
Mail::assertSent(MyEmail::class, function ($mail) use ($foo) { 
    return $mail->foo->id == $foo->id; 
});

从技术上讲,这些测试并不完全符合单元测试的要求,因为它们会涉及到数据库操作,但是它们应该足以覆盖控制器和事件。

如果要使它们成为真正的单元测试,您需要实现仓储模式来处理数据库查询,而不是直接使用Eloquent,并模拟仓储,以便您可以断言模拟的仓储接收了正确的数据并返回模型的模拟。 Mockery 对此非常有用。


很好的解释! - Damon
这是一个很好的解释,配有实际的例子。它强调了单元测试应该尽可能地包裹最小的单元的方法。 - Jason

1
您没有触发事件FooEvent,而是FooCreated,并且您没有调用控制器上实际分派事件的方法(至少在您显示的代码中没有)。
// 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;
    });

}

如果您不想一直触发事件,可以添加一个新的方法:
// Foo model
public static function createWithEvent(array $attributes = [])
{
    $model = parent::create($attributes);

    event(new FooCreated($foo));

    return $model;

}

事件位于控制器方法内部。我不能为测试此事件创建一个单独的测试方法吗?或者作为一个特性? - ltdev
1
你最好保持你的控制器瘦身,并将逻辑放在模型中并进行测试。否则,你需要模拟请求并测试控制器方法(你没有这样做,因此无法看到事件)。 - Mike Miller
我想在控制器中保留事件的原因是因为我还在其他地方使用了 Foo::create,而我不想在此时触发该事件。 - ltdev
只需向您的模型添加一个新方法即可。 - Mike Miller
@Lykos,我进行了编辑以展示这个过程是多么简单。 - Mike Miller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接