Mock должен вызываться ровно 1 раз, но вызываться 0 раз

У меня странная проблема с Laravel 5 и PHPUnit. Когда я пытаюсь издеваться над фасадами Laravel (например, Auth, View, Mail), я всегда получаю это исключение:

Mockery\Exception\InvalidCountException: метод send(emails.register, array('user'=›'object(MCC\Models\Users\User)',), object(Closure)) из Mockery_0_Illuminate_Mail_Mailer должен вызываться ровно 1 раз, но вызывается 0 раз.

У меня проблема с частью "should be called exactly 1 times but called 0 times.". Это мой тестовый код:

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

Я поставил dd($message) в конце, потому что хочу узнать подробности о возвращаемом значении (как выглядит $message->getTo()).

Мой класс TestCase:

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

Мой 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>

Я проверил много источников из Google, из Stackoverflow, большинство людей упомянули о

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

но даже эта инструкция не помогает. Большинство вопросов по этой проблеме не решены. Я проверил установленные расширения, мой Laravel только что установлен (некоторые модели, некоторые маршруты, около 20 тестов).

Также я пробовал такие методы, как

->atLeast()
->times(1)

or

->atLeast()
->once()

но ничего не работает должным образом. Также вместо

Mail::shouldReceive('mail')

я использовал

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

но эти методы все еще не работают.

Остальной журнал консоли:

/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

Также я нашел один отличный совет (там Stackoverflow), но он не работает.

Mockery по умолчанию является библиотекой-заглушкой, а не фиктивной (что сбивает с толку из-за ее названия).

Это означает, что -›shouldReceive(...) по умолчанию используется ноль или более раз. При использовании -›once() вы говорите, что она должна быть вызвана ноль или один раз, но не больше. Это значит, что это всегда пройдет.

Если вы хотите подтвердить, что он вызывается один раз, вы можете использовать -›atLeast()-›times(1) (один или несколько раз) или -›times(1) (ровно один раз)

Моя версия php: PHP 5.6.14-1+deb.sury.org~trusty+1 (cli)

Мой апач: Server version: Apache/2.4.16 (Ubuntu)

Насмешливая версия (от композитора): "mockery/mockery": "0.9.*"

Фреймворк Laravel (от композитора): "laravel/framework": "5.1.*"


person Grzegorz Gajda    schedule 11.10.2015    source источник


Ответы (2)


Глядя на ваш тестовый пример:

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

Либо:

  • Создание пользователя вызывает фасад Mail, и в этом случае вы вызываете этот фасад перед созданием макета.

OR

  • Вы не вызываете функцию, которая вызывает фасад Mail.

В любом случае Mail::shouldReceive('send') не должно быть не последним в тестовом примере.

Ошибка, которую вы получаете, связана с тем, что Mail ожидает, что вызов произойдет после вызова ::shouldRecieve(), но это не так — тестовый пример заканчивается, а экземпляр Mockery никогда не вызывался.

Вместо этого вы можете попробовать что-то вроде этого:

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();
}
person samlev    schedule 20.10.2015

Чтобы интегрировать Mockery с PhpUnit, вам просто нужно определить метод tearDown() для ваших тестов, содержащий следующее:

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

Этот статический вызов очищает контейнер Mockery, используемый текущим тестом, и запускает любые задачи проверки, необходимые для ваших ожиданий.

Дополнительную информацию об интеграции Mockery и PhpUnit можно найти здесь.

person Alex Kyriakidis    schedule 12.10.2015
comment
Встроенный метод Laravel для tearDown() имеет Mockery::close(); и все равно не помогает. Наследование родительского метода tearDown() и добавление функции в начало (или конец) тоже не помогает. Из документов Mockery я беру слушателя и помещаю его в свой phpunit.xml, но он все равно не работает. Все то же исключение. - person Grzegorz Gajda; 12.10.2015
comment
Вы пытались закрыть насмешку без наследования родительского закрытия? - person Alex Kyriakidis; 12.10.2015
comment
да, во многих комбинациях. Я поместил Mockery::close() в конец метода, пользовательский tearDown() в класс TestCase, пользовательский tearDown() в тестовый класс и, конечно же, смешал эти комбинации. Все то же исключение. - person Grzegorz Gajda; 12.10.2015