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

Mail:fake()/Queue::fake() not working with Mail::queue() #15919

Closed
felorhik opened this issue Oct 14, 2016 · 11 comments
Closed

Mail:fake()/Queue::fake() not working with Mail::queue() #15919

felorhik opened this issue Oct 14, 2016 · 11 comments

Comments

@felorhik
Copy link

felorhik commented Oct 14, 2016

  • 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();
}
@maximusblade
Copy link

experiencing something similar with an existing package.

@themsaid
Copy link
Member

themsaid commented Nov 3, 2016

PR submitted with a fix #16248

@themsaid themsaid closed this as completed Nov 3, 2016
@andzandz
Copy link

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

@andzandz
Copy link

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

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

@arnabrahman
Copy link

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

@hootlex
Copy link

hootlex commented Feb 25, 2017

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

@VinceG
Copy link
Contributor

VinceG commented Mar 3, 2017

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

@themsaid
Copy link
Member

themsaid commented Mar 3, 2017

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

@leandroruel
Copy link

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

@devcircus
Copy link
Contributor

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,

@haddocc
Copy link

haddocc commented Jul 15, 2019

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

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

10 participants