Mockery для PHP: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Mockery для PHP: полное руководство с примерами

1. Что это и зачем нужна

Mockery — это мощная и гибкая библиотека для создания мок-объектов (mock objects) в PHP. Она предназначена для использования в модульном тестировании (unit testing) и позволяет разработчикам изолировать тестируемый код от его зависимостей. Вместо того чтобы использовать реальные объекты (например, подключение к базе данных, API-клиенты или файловую систему), вы создаете их "заглушки" (mocks), которые имитируют поведение оригиналов. Это делает тесты быстрыми, надежными и независимыми от внешних факторов.

Зачем это нужно? Представьте, что вы тестируете класс OrderProcessor, который отправляет email после оформления заказа. Если вы будете использовать реальный Mailer, ваш тест станет медленным, будет зависеть от работоспособности SMTP-сервера и может случайно отправить письма реальным пользователям. С помощью Mockery вы создаете "фальшивый" Mailer, который просто проверяет, был ли вызван метод send() с правильными аргументами. Это позволяет сосредоточиться на логике OrderProcessor, а не на инфраструктуре.

Mockery — это не просто замена объектов. Она предоставляет богатый API для описания ожиданий (expectations): вы можете указать, сколько раз должен быть вызван метод, с какими аргументами, в каком порядке, и что он должен возвращать. Это делает тесты не только изолированными, но и самодокументируемыми, так как ожидания явно описывают взаимодействие между объектами.

2. Установка

Mockery устанавливается через Composer — стандартный менеджер пакетов для PHP. Рекомендуется устанавливать её как зависимость для разработки (require-dev), так как она не нужна в production-окружении.

composer require --dev mockery/mockery

После установки убедитесь, что ваш файл composer.json содержит следующую запись в секции require-dev:

{
    "require-dev": {
        "mockery/mockery": "^1.6"
    }
}

Если вы используете PHPUnit, Mockery интегрируется с ним "из коробки" через трейт \Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration.

3. Быстрый старт

Давайте создадим простой тест, который демонстрирует базовую функциональность Mockery. Предположим, у нас есть класс Calculator, который зависит от сервиса Logger.

<?php
// Классы для примера
interface Logger {
    public function log(string $message): void;
}

class Calculator {
    private Logger $logger;

    public function __construct(Logger $logger) {
        $this->logger = $logger;
    }

    public function add(int $a, int $b): int {
        $result = $a + $b;
        $this->logger->log("Adding $a and $b, result: $result");
        return $result;
    }
}

// Тест с использованием Mockery
use PHPUnit\Framework\TestCase;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;

class CalculatorTest extends TestCase {
    use MockeryPHPUnitIntegration;

    public function testAddLogsCorrectly(): void {
        // 1. Создаем мок для Logger
        $loggerMock = Mockery::mock(Logger::class);

        // 2. Описываем ожидание: метод log() должен быть вызван 1 раз
        //    с конкретным сообщением
        $loggerMock->shouldReceive('log')
            ->once()
            ->with('Adding 2 and 3, result: 5');

        // 3. Создаем тестируемый объект с моком
        $calculator = new Calculator($loggerMock);

        // 4. Выполняем действие
        $result = $calculator->add(2, 3);

        // 5. Проверяем результат
        $this->assertEquals(5, $result);

        // 6. Mockery автоматически проверяет, что все ожидания выполнены
        //    при завершении теста (благодаря трейту MockeryPHPUnitIntegration)
    }
}

В этом примере мы создали мок для интерфейса Logger, указали, что метод log должен быть вызван ровно один раз с аргументом 'Adding 2 and 3, result: 5'. Если бы метод не был вызван или был вызван с другим аргументом, тест бы провалился.

4. Основные методы и функции

Ниже приведены ключевые методы Mockery, которые покрывают 90% потребностей при написании тестов. Для каждого метода указана сигнатура, описание и рабочий пример.

4.1. Mockery::mock()

Сигнатура: Mockery::mock(string|object $class, array $constructorArgs = []) : MockInterface

Что делает: Создает мок-объект для указанного класса, интерфейса или трейта. Можно передать имя класса (строку) или готовый объект. Вторым параметром можно передать аргументы для конструктора, если они нужны.

// Создание мока для интерфейса
$mock = Mockery::mock(UserRepositoryInterface::class);

// Создание мока для конкретного класса с аргументами конструктора
$mock = Mockery::mock(Logger::class, ['log-file.txt']);

// Создание мока на основе существующего объекта (частичный мок)
$realObject = new SomeClass();
$mock = Mockery::mock($realObject);

4.2. shouldReceive()

Сигнатура: $mock->shouldReceive(string|array $methods) : ExpectationInterface

Что делает: Начинает цепочку определения ожиданий для одного или нескольких методов. Возвращает объект ожидания, на котором можно вызывать другие методы (например, once(), with()).

$mock = Mockery::mock(Service::class);

// Ожидание для одного метода
$mock->shouldReceive('process');

// Ожидание для нескольких методов (массив)
$mock->shouldReceive(['process', 'validate', 'save']);

// Можно вызывать несколько раз для разных методов
$mock->shouldReceive('fetch')->andReturn('data');
$mock->shouldReceive('delete')->andReturn(true);

4.3. once(), twice(), times()

Сигнатура: $expectation->once() : self, $expectation->twice() : self, $expectation->times(int $n) : self

Что делает: Определяют, сколько раз должен быть вызван метод. once() — ровно 1 раз, twice() — 2 раза, times(n) — ровно n раз. Если не указать, метод может быть вызван любое количество раз (включая 0).

$mock = Mockery::mock(Queue::class);

// Метод push() должен быть вызван ровно 1 раз
$mock->shouldReceive('push')->once();

// Метод flush() должен быть вызван ровно 3 раза
$mock->shouldReceive('flush')->times(3);

// Метод clear() должен быть вызван ровно 2 раза
$mock->shouldReceive('clear')->twice();

4.4. with()

Сигнатура: $expectation->with(mixed ...$args) : self

Что делает: Указывает, с какими аргументами должен быть вызван метод. Если не использовать with(), метод примет любые аргументы. Можно передавать конкретные значения, замыкания для проверки или специальные хэлперы (например, Mockery::any()).

$mock = Mockery::mock(EmailService::class);

// Точное совпадение аргументов
$mock->shouldReceive('send')
    ->with('user@example.com', 'Subject', 'Body text');

// Использование замыкания для проверки
$mock->shouldReceive('send')
    ->with(Mockery::on(function($email) {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }));

// Любой аргумент
$mock->shouldReceive('send')
    ->with(Mockery::any(), Mockery::any());

4.5. andReturn(), andReturnNull(), andThrow()

Сигнатура: $expectation->andReturn(mixed ...$values) : self, $expectation->andReturnNull() : self, $expectation->andThrow(Throwable|string $exception) : self

Что делает: Определяют, что должен вернуть метод при вызове. andReturn() возвращает указанное значение (можно передать несколько для последовательных вызовов). andReturnNull() возвращает null. andThrow() выбрасывает исключение.

$mock = Mockery::mock(UserRepository::class);

// Всегда возвращать одно и то же значение
$mock->shouldReceive('findById')
    ->with(1)
    ->andReturn(new User('John'));

// Разные значения для последовательных вызовов
$mock->shouldReceive('getNextId')
    ->andReturn(1, 2, 3); // Первый вызов вернет 1, второй 2, третий 3

// Выброс исключения
$mock->shouldReceive('save')
    ->andThrow(new \RuntimeException('Database connection failed'));

// Возврат null
$mock->shouldReceive('findById')
    ->with(999)
    ->andReturnNull();

4.6. andReturnUsing()

Сигнатура: $expectation->andReturnUsing(callable $callback) : self

Что делает: Позволяет динамически вычислять возвращаемое значение на основе аргументов, с которыми был вызван метод. Принимает замыкание, которое получает те же аргументы, что и оригинальный метод.

$mock = Mockery::mock(PriceCalculator::class);

// Возвращаем цену в зависимости от количества
$mock->shouldReceive('calculate')
    ->andReturnUsing(function(int $quantity, float $unitPrice) {
        if ($quantity > 10) {
            return $quantity * $unitPrice * 0.9; // 10% скидка
        }
        return $quantity * $unitPrice;
    });

$price1 = $mock->calculate(5, 100.0); // 500.0
$price2 = $mock->calculate(15, 100.0); // 1350.0

4.7. shouldNotReceive()

Сигнатура: $mock->shouldNotReceive(string|array $methods) : MockInterface

Что делает: Указывает, что указанный метод не должен быть вызван ни при каких обстоятельствах. Если метод будет вызван, тест провалится. Полезно для проверки, что определенные действия не выполняются.

$mock = Mockery::mock(PaymentGateway::class);

// Метод charge() не должен вызываться
$mock->shouldNotReceive('charge');

// Несколько методов
$mock->shouldNotReceive(['charge', 'refund']);

4.8. shouldHaveReceived() (для частичных моков и шпионов)

Сигнатура: $mock->shouldHaveReceived(string $method, array $args = []) : void

Что делает: Проверяет, что метод был вызван после выполнения тестируемого кода. Используется в основном со шпионами (spies) или частичными моками, когда вы не хотите заранее объявлять ожидания, а проверяете их постфактум.

$mock = Mockery::mock(Logger::class);

// Выполняем код, который может вызвать метод
$processor = new OrderProcessor($mock);
$processor->processOrder(123);

// Проверяем, что метод log() был вызван с определенными аргументами
$mock->shouldHaveReceived('log')
    ->with('Order 123 processed successfully');

// Проверяем, что метод был вызван ровно 2 раза
$mock->shouldHaveReceived('log')
    ->times(2);

4.9. shouldIgnoreMissing()

Сигнатура: $mock->shouldIgnoreMissing() : MockInterface

Что делает: Делает мок "терпимым" к вызовам методов, для которых не определены ожидания. Вместо выбрасывания исключения, такой мок будет возвращать null (или пустой объект для методов, возвращающих объекты). Полезно, когда вы хотите проверить только часть взаимодействий.

$mock = Mockery::mock(ComplexService::class);
$mock->should                    

Рекомендаций