Skip to content

Instantly share code, notes, and snippets.

@NoelDeMartin
Created November 30, 2017 08:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NoelDeMartin/60b70e81b483e9393aaf06cafb72e8ec to your computer and use it in GitHub Desktop.
Save NoelDeMartin/60b70e81b483e9393aaf06cafb72e8ec to your computer and use it in GitHub Desktop.
Laravel Dusk Mail mocking proposal

This gist is a solution to Laravel Dusk issue 152. I have been using this approach for some time and it has been working without any problems. It specifically solves mocking Mail services in Dusk tests, but the same approach could be used to fake Queue and any other services.

This implementation is based on how Dusk already handles login sessions, this can be seen in DuskServiceProvider.php:18..31. Some special routes (starting with _) are used to comunicate with the browser and set some cookies, this cookies are then read every time a test is executed in the browser and the mocking data is serialized/deserialized within the cookie. These test providers should only be enabled on testing environment, or else all this operations would be an unecessary overhead in each request.

The reason why this is a gist and not a pull request for dusk is that this is the approach I have been using in my projects and, although I think it's a good approach there are some things to do before this can be added to dusk:

  • Have a generic interface / pattern for doing this (as I said, not only Mail could be mocked like this, but any service). This should be extendible for any custom services or any other Laravel services.
  • Settle on a nomenclature. I am aware the naming in some classes is not ideal (for example having the service "enabled" and "active" is not understandable in my opinion).
  • Complete the implementation of Mail mocking. I have only implemented what is necessary for my tests, but a pull request should be a complete implementation of Laravel mailing.
  • Have some kind of feedback from Dusk maintainers. I'd like to know that this is an acceptable approach before proceeding, since it would be a waste to implement all this for it to be rejected right away.

I want to do this when I have the time and provide a pull request (no ETA at the moment). In the meantime, any comments or implementation proposals are welcome.

<?php
namespace Tests\Browser\Dusk;
use Tests\Testing\MailFake;
use Laravel\Dusk\Browser as BaseBrowser;
class Browser extends BaseBrowser {
public function fake($service) {
switch ($service) {
case 'mail':
return $this->visit('/_test/mail/fake');
}
}
public function getServiceFake($service) {
switch($service) {
case 'mail':
$response = $this->visit('/_test/mail/data');
$data = json_decode(strip_tags($response->driver->getPageSource()), true);
$fake = new MailFake;
$fake->update($data);
return $fake;
}
}
}
<?php
namespace Tests\Browser;
use Mail;
use Tests\DuskTestCase;
use Tests\Browser\Dusk\Browser;
class ExampleTest extends DuskTestCase
{
protected function newBrowser($driver) {
return new Browser($driver);
}
public function testMail()
{
$this->browse(function (Browser $browser) {
$browser->fake('mail');
// Execute actions which send an email.
Mail::swap($browser->getServiceFake('mail'));
// Email assertions as usual
});
}
}
<?php
namespace Tests\Browser\Services;
use Mail;
use Cache;
use Cookie;
use Illuminate\Support\Str;
use Tests\Testing\MailFake;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Support\ServiceProvider;
class FakeMailService implements Mailer {
const COOKIE_NAME = 'FakeMail';
private $cacheId;
private $fake;
public function __construct() {
$this->fake = new MailFake;
}
public function getFake() {
return $this->fake;
}
public function enable() {
if (!$this->isEnabled()) {
$cookie = Cookie::make(static::COOKIE_NAME, $this->generateCacheId(), 0, '/');
Cookie::queue($cookie);
}
}
public function isEnabled() {
return !is_null(Cookie::get(static::COOKIE_NAME));
}
public function activate() {
$this->cacheId = Cookie::get(static::COOKIE_NAME);
if (Cache::has($this->cacheId)) {
$this->fake->update(Cache::get($this->cacheId));
}
Mail::swap($this);
}
/* Mailer interface */
public function to($users) {
$this->fake->to($users);
$this->updateCache();
}
public function bcc($users) {
$this->fake->bcc($users);
$this->updateCache();
}
public function raw($text, $callback) {
$this->fake->raw($text, $callback);
$this->updateCache();
}
public function send($view, array $data = [], $callback = null) {
$this->fake->send($view, $data, $callback);
$this->updateCache();
}
public function failures() {
$this->fake->failures();
$this->updateCache();
}
/* Private methods */
private function updateCache() {
Cache::put($this->cacheId, $this->fake->serialize(), 10);
}
private function generateCacheId() {
return static::COOKIE_NAME . ':' . Str::random(40);
}
}
<?php
namespace Tests\Browser\Providers;
use Illuminate\Support\ServiceProvider;
use Tests\Browser\Services\FakeMailService;
class FakeMailServiceProvider extends ServiceProvider {
public function boot() {
$service = new FakeMailService;
$this->app->singleton('fake-mail', function() use ($service) {
return $service;
});
$service = $this->app->make('fake-mail');
if ($service->isEnabled()) {
$service->activate();
}
}
}
<?php
namespace Tests\Testing;
use Illuminate\Support\Testing\Fakes\MailFake as BaseFake;
class MailFake extends BaseFake {
public function update($data) {
$this->mailables = $data['mailables'];
}
public function serialize() {
return [
'mailables' => $this->mailables
];
}
}
<?php
namespace Tests\Browser\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Tests\Browser\Services\FakeMailService;
use Tests\Browser\Providers\FakeMailServiceProvider;
class TestsServiceProvider extends ServiceProvider {
public function boot() {
Route::middleware('web')
->group(function() {
Route::get('/_test/mail/fake', function() {
return app('fake-mail')->enable();
});
Route::get('/_test/mail/data', function() {
return app('fake-mail')->getFake()->serialize();
});
});
$this->app->registerDeferredProvider(FakeMailServiceProvider::class, 'mailer');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment