Framework: Mail:fake()/Queue::fake() not working with Mail::queue()

Created on 15 Oct 2016  路  11Comments  路  Source: laravel/framework

  • Laravel Version: 5.3.18
  • PHP Version: 7.0
  • Database Driver & Version: mariadb Ver 15.1 Distrib 10.1.17-MariaDB,

    Description:

It seems that when using Mail::fake() in a test and the controller / method uses Mail::queue() it fails due to the queue function not being available on the MailFake class.

Symfony\Component\Debug\Exception\FatalThrowableError: Call to undefined method Illuminate\Support\Testing\Fakes\MailFake::queue()

I have also tried Queue::fake() however Mailables do not use the correct interface to fake the queue.

Argument 1 passed to Illuminate\Mail\Mailer::setQueue() must implement interface Illuminate\Contracts\Queue\Factory, instance of Illuminate\Support\Testing\Fakes\QueueFake given

I do understand that mail going to a queue won't be treated normally as is. However if it may or may not send it to a queue based on conditional logic in the controller this can raise a fairly significant problem. Also due to Queue::fake() failing if you try to queue mail and send a normal job to a queue the job will fail.

I tried to look into the source, but could not accurately grasp how it all works. Will probably continue to look through it though.

Steps To Reproduce:

Try to use Mail::fake() for a test with a controller using a Mailable in Mail::queue().

Use Queue::fake() instead of Mail::fake() for the same as above.

Controller

public function somefakeroute()
{
   Mail::queue(new Mailable());
   return 'view';
}

Test

public function testMailFake()
{
   Mail::fake();
   //Queue::fake(); //toggle between to test
   $this->route('GET', 'somefakeroute');
   $this->assertResponseOk();
}

Most helpful comment

for laravel 5.4 mail::queue works just fine but for lumen 5.4 it gives this error. Frustrating

All 11 comments

experiencing something similar with an existing package.

@themsaid I seem to be having the same problem with laravel/framework 5.3.25 which includes your PR

On a clean Laravel install, I tracked it down to the following two statements and wrote this demo test:

    public function testQueueFake()
    {
        Queue::fake();

        $mailer = app(Mailer::class);
    }

I get:

There was 1 error:

1) ExampleTest::testQueueFake2
TypeError: Argument 1 passed to Illuminate\Mail\Mailer::setQueue() must be an instance of Illuminate\Contracts\Queue\Factory, instance of Illuminate\Support\Testing\Fakes\QueueFake given, called in /home/vagrant/code/laravel/api/spike/vendor/laravel/framework/src/Illuminate/Mail/MailServiceProvider.php on line 67

/home/vagrant/code/laravel/api/spike/vendor/laravel/framework/src/Illuminate/Mail/Mailer.php:480
/home/vagrant/code/laravel/api/spike/vendor/laravel/framework/src/Illuminate/Mail/MailServiceProvider.php:67
/home/vagrant/code/laravel/api/spike/vendor/laravel/framework/src/Illuminate/Mail/MailServiceProvider.php:34
/home/vagrant/code/laravel/api/spike/vendor/laravel/framework/src/Illuminate/Container/Container.php:746
/home/vagrant/code/laravel/api/spike/vendor/laravel/framework/src/Illuminate/Container/Container.php:644
/home/vagrant/code/laravel/api/spike/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:709
/home/vagrant/code/laravel/api/spike/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php:106
/home/vagrant/code/laravel/api/spike/tests/ExampleTest.php:41

Here is setQueue in Illuminate/Mail/Mailer:

    public function setQueue(QueueContract $queue)
    {
        $this->queue = $queue;

        return $this;
    }

Remove the typehint and it all seems to work:

    public function setQueue($queue)
    {
        $this->queue = $queue;

        return $this;
    }

Another solution is to add the interface to Illuminate\Support\Testing\Fakes\QueueFake:

class QueueFake implements Queue, \Illuminate\Contracts\Queue\Factory
{

for laravel 5.4 mail::queue works just fine but for lumen 5.4 it gives this error. Frustrating

I am facing the issue with Queue::fake() on Laravel 5.4.13 with beanstalkd driver.

@themsaid I believe this is still an issue. I can open a new issue if needed with a test case.

@VinceG if on 5.4 then yes go ahead and open an issue with more details please. thanks :)

just a few months left to 2018 and this issue still open... :disappointed:

See #20454. Looks like any remaining unsolved issues should be covered. If there's an issue in the current release of Laravel, submit an new issue. This one will not get any attention since it's closed,

This might be too late for most people, but if you're still bound to Laravel 5.3 here's my solution.

I was using Mailables implementing the ShouldQueue interface and not Mail:queue, and was having Major problems with Queue:fake in tests.

It's just not suited for testing Mailables but with a few adjustments you can make it work.

First after calling Queue::fake you have to set the Queue on the Mail facade:

Mail::setQueue(Queue::getFacadeRoot());

Otherwise it would still use the QueueManager instead of QueueFake

Then you have to assert whether Illuminate\Mail\SendQueuedMailable was pushed to the queue.
However this has the downside that you can't see which Mailable is being pushed. The mailable is stored though in a protected variable. So you have to make your own custom SendQueuedMailable
and add a method that will expose this variable.

namespace App\Mail\Base;

use Illuminate\Mail\SendQueuedMailable as BaseSendQueuedMailable;

class SendQueuedMailable extends BaseSendQueuedMailable
{
    public function getMailable()
    {
        return $this->mailable;
    }
}

You would also have to override some functions in the Mailable class and include that instead of Illuminate\Mail\Mailable inside your concrete Mailables.

namespace App\Mail\Base;

use Illuminate\Container\Container;
use Illuminate\Contracts\Mail\Mailer as MailerContract;
use Illuminate\Contracts\Queue\Factory as Queue;
use App\Mail\Base\SendQueuedMailable as MySendQueuedMailable;

class Mailable extends \Illuminate\Mail\Mailable
{
    /**
     * Queue the message for sending.
     *
     * @param  \Illuminate\Contracts\Queue\Factory  $queue
     * @return mixed
     */
    public function queue(Queue $queue)
    {
        $connection = property_exists($this, 'connection') ? $this->connection : null;

        $queueName = property_exists($this, 'queue') ? $this->queue : null;

        if ($queueName) {
            return $queue->connection($connection)->pushOn(
                $queueName, new MySendQueuedMailable($this)
            );
        } else {
            return $queue->connection($connection)->push(
                new MySendQueuedMailable($this)
            );
        }
    }

    /**
     * Deliver the queued message after the given delay.
     *
     * @param  \DateTime|int  $delay
     * @param  Queue  $queue
     * @return mixed
     */
    public function later($delay, Queue $queue)
    {
        $connection = property_exists($this, 'connection') ? $this->connection : null;

        $queueName = property_exists($this, 'queue') ? $this->queue : null;

        if ($queueName) {
            return $queue->connection($connection)->laterOn(
                $queueName, $delay, new MySendQueuedMailable($this)
            );
        } else {
            return $queue->connection($connection)->later(
                $delay, new MySendQueuedMailable($this)
            );
        }
    }
}

You will also have to extend QueueFake and the Queue facade, because the system will whine that QueueFake doesn't implement Illuminate\Contracts\Queue\Factory:

namespace App\Mail\Base\Tests;

use Illuminate\Support\Testing\Fakes\QueueFake as BaseQueueFake;
use Illuminate\Contracts\Queue\Factory as QueueContract;

class QueueFake extends BaseQueueFake implements QueueContract
{

}

This doesn't give any problems because QueueFake implicitly already implements Illuminate\Contracts\Queue\Factory since it has a connection method.

My Queue facade:

namespace App\Mail\Base\Tests;

use App\Mail\Base\Tests\QueueFake as MyQueueFake;
use Illuminate\Support\Facades\Queue as BaseQueue;

class Queue extends BaseQueue
{
    /**
     * Replace the bound instance with a fake.
     *
     * @return void
     */
    public static function fake()
    {
        static::swap(new MyQueueFake);
    }
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

shopblocks picture shopblocks  路  3Comments

gabriellimo picture gabriellimo  路  3Comments

ghost picture ghost  路  3Comments

JamborJan picture JamborJan  路  3Comments

CupOfTea696 picture CupOfTea696  路  3Comments