7
votes

I have weird problem with Laravel 5 and PHPUnit. When I try to mock Laravel's facades (e.g. Auth, View, Mail) I always get this exception:

Mockery\Exception\InvalidCountException: Method send("emails.register", array('user'=>'object(MCC\Models\Users\User)',), object(Closure)) from Mockery_0_Illuminate_Mail_Mailer should be called exactly 1 times but called 0 times.

I have a problem with "should be called exactly 1 times but called 0 times." part. This is my test code:

public function testSendEmailToNewUserListener()
{
    $user = factory(MCC\Models\Users\User::class)->create();

    Mail::shouldReceive('send')
        ->with(
            'emails.register',
            ['user' => $user],
            function ($mail) use ($user) {
               $mail->to($user->email, $user->name)
                    ->subject('Thank you for registering an account.');
            }
        )
        ->times(1)
        ->andReturnUsing(function ($message) use ($user) {
            dd($message);
            $this->assertEquals('Thank you for registering an account.', $message->getSubject());
            $this->assertEquals('mcc', $message->getTo());
            $this->assertEquals(View::make('emails.register'), $message->getBody());
        });
}

I put dd($message) in closure because I want to know details about return value (how looks $message->getTo()).

My TestCase class:

<?php

/**
 * Provides default values for functional tests.
 *
 * Class TestCase
 */
abstract class TestCase extends Illuminate\Foundation\Testing\TestCase
{
    /**
     * The base URL to use while testing the application.
     *
     * @var string
     */
    protected $baseUrl = 'http://004-mcc.dev';

    /**
     * Creates the application.
     *
     * @return \Illuminate\Foundation\Application
     */
    public function createApplication()
    {
        $app = require __DIR__ . '/../bootstrap/app.php';

        $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

        \Illuminate\Support\Facades\Mail::pretend(TRUE);

        return $app;
    }
}

My phpunit.xml:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
     backupStaticAttributes="false"
     bootstrap="bootstrap/autoload.php"
     colors="true"
     convertErrorsToExceptions="true"
     convertNoticesToExceptions="true"
     convertWarningsToExceptions="true"
     processIsolation="false"
     stopOnFailure="false"
     syntaxCheck="false">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>./tests/</directory>
        </testsuite>
        <testsuite name="User">
            <directory>./tests/UserRepository</directory>
        </testsuite>
        <testsuite name="User/Auth">
            <directory>./tests/UserRepository/Auth</directory>
        </testsuite>
        <testsuite name="User/User">
            <directory>./tests/UserRepository/User</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory suffix=".php">app/</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="local"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
    </php>
</phpunit>

I checked many sources from Google, from Stackoverflow, most people mentioned about

$this->app->instance('Illuminate\Mail\Mailer', $mockMailer)

but even this instruction doesn't help. Most of questions about this problem are not solved. I checked installed extensions, my Laravel is fresh installed (some models, some routes, about 20 tests).

Also I tried methods like

->atLeast()
->times(1)

or

->atLeast()
->once()

but nothing is works properly. Also instead of

Mail::shouldReceive('mail')

I used

$mailMock = Mockery::mock('Illuminate\Mail\Mailer');
$mailMock->shouldReceive('mail)

but these methods still not work.

Rest of console log:

/home/grzgajda/programowanie/php/005mcc/vendor/mockery/mockery/library/Mockery/CountValidator/Exact.php:37
/home/grzgajda/programowanie/php/005mcc/vendor/mockery/mockery/library/Mockery/Expectation.php:271
/home/grzgajda/programowanie/php/005mcc/vendor/mockery/mockery/library/Mockery/ExpectationDirector.php:120
/home/grzgajda/programowanie/php/005mcc/vendor/mockery/mockery/library/Mockery/Container.php:297
/home/grzgajda/programowanie/php/005mcc/vendor/mockery/mockery/library/Mockery/Container.php:282
/home/grzgajda/programowanie/php/005mcc/vendor/mockery/mockery/library/Mockery.php:142
/home/grzgajda/programowanie/php/005mcc/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:48
/home/grzgajda/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:148
/home/grzgajda/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:100

Also I found one great advice (there Stackoverflow) but it doesn't work.

Mockery is by default a stubbing library, not a mocking one (which is confusing because of its name).

That means that ->shouldReceive(...) by default is "zero or more times". When using ->once(), you say it should be called zero or one time, but not more. This means it'll always pass.

When you want to assert that it is called once, you can use ->atLeast()->times(1) (one or more times) or ->times(1) (exactly one time)

My php version: PHP 5.6.14-1+deb.sury.org~trusty+1 (cli)

My apache: Server version: Apache/2.4.16 (Ubuntu)

Mockery version (from composer): "mockery/mockery": "0.9.*"

Laravel framework (from composer): "laravel/framework": "5.1.*"

2

2 Answers

11
votes

Looking at your test case:

public function testSendEmailToNewUserListener()
{
    $user = factory(MCC\Models\Users\User::class)->create();

    Mail::shouldReceive('send')
        ->with(
            'emails.register',
            ['user' => $user],
            function ($mail) use ($user) {
               $mail->to($user->email, $user->name)
                    ->subject('Thank you for registering an account.');
            }
        )
        ->times(1)
        ->andReturnUsing(function ($message) use ($user) {
            dd($message);
            $this->assertEquals('Thank you for registering an account.', $message->getSubject());
            $this->assertEquals('mcc', $message->getTo());
            $this->assertEquals(View::make('emails.register'), $message->getBody());
        });
}

Either:

  • Creating the user calls the Mail facade, in which case you're calling that facade before you're mocking it up.

OR

  • You aren't calling the function which calls the Mail facade.

Either way, Mail::shouldReceive('send') should not be the last thing in the test case.

The error you are getting is because Mail is expecting the call to happen after you call ::shouldRecieve(), but it's not - the test case ends, and the Mockery instance has never been called.

You could try something like this instead:

public function testSendEmailToNewUserListener()
{   
    $testCase = $this;

    Mail::shouldReceive('send')
        ->times(1)
        ->andReturnUsing(function ($message) use ($testCase) {
            $testCase->assertEquals('Thank you for registering an account.', $message->getSubject());
            $testCase->assertEquals('mcc', $message->getTo());
            $testCase->assertEquals(View::make('emails.register'), $message->getBody());
        });

    $user = factory(MCC\Models\Users\User::class)->create();
}
2
votes

To integrate Mockery with PhpUnit, you just need to define a tearDown() method for your tests containing the following:

public function tearDown() {
    \Mockery::close();
}

This static call cleans up the Mockery container used by the current test, and run any verification tasks needed for your expectations.

You can find more information about Mockery and PhpUnit integation here.