Prophecy для PHP: полное руководство с примерами
1. Что это и зачем нужна
Prophecy — это мощная объектная библиотека для создания моков (mock-объектов) в PHP. Она предоставляет элегантный, выразительный и гибкий API для создания тестовых заглушек, шпионов и поддельных объектов, которые имитируют поведение реальных зависимостей в модульных тестах. В отличие от многих других библиотек, Prophecy фокусируется на "пророчестве" (prophecy) — вы описываете, как объект должен себя вести, а библиотека автоматически генерирует реализацию.
Основная цель Prophecy — сделать написание модульных тестов максимально простым и читаемым. Вместо того чтобы вручную создавать сложные mock-объекты с кучей конфигураций, вы просто "объявляете пророчество" для нужного класса или интерфейса, а затем вызываете методы, которые должны быть вызваны в тестируемом коде. Prophecy автоматически проверяет, соответствуют ли реальные вызовы вашим ожиданиям.
Библиотека особенно полезна при тестировании кода, который зависит от внешних сервисов (базы данных, API, файловой системы), сложных объектов или объектов с побочными эффектами. Prophecy позволяет изолировать тестируемый код, заменяя реальные зависимости на предсказуемые заглушки. Она является стандартом де-факто для многих PHP-фреймворков, включая Symfony и Laravel, и используется в таких популярных проектах как Doctrine и PHPUnit (через интеграцию).
2. Установка
Prophecy устанавливается через Composer — стандартный менеджер зависимостей для PHP. Выполните следующую команду в корне вашего проекта:
composer require --dev phpspec/prophecy
Флаг --dev означает, что библиотека будет добавлена только в секцию require-dev вашего composer.json, так как Prophecy нужна исключительно для тестирования и не должна использоваться в production-окружении.
Если вы используете PHPUnit, рекомендуется также установить интеграционный пакет для автоматической работы с Prophecy:
composer require --dev phpspec/prophecy-phpunit
После установки убедитесь, что файл vendor/autoload.php подключен в вашем тестовом файле (обычно это делается автоматически в PHPUnit).
3. Быстрый старт — минимальный рабочий пример
Давайте напишем простейший тест с использованием Prophecy. Предположим, у нас есть класс UserMailer, который отправляет приветственное письмо новому пользователю. Он зависит от интерфейса MailerInterface.
<?php
// Классы для тестирования
interface MailerInterface {
public function send(string $to, string $subject, string $body): bool;
}
class UserMailer {
private $mailer;
public function __construct(MailerInterface $mailer) {
$this->mailer = $mailer;
}
public function sendWelcomeEmail(string $email): bool {
return $this->mailer->send(
$email,
'Welcome!',
'Thank you for joining!'
);
}
}
// Тест с использованием Prophecy
use Prophecy\PhpUnit\ProphecyTrait;
use PHPUnit\Framework\TestCase;
class UserMailerTest extends TestCase {
use ProphecyTrait;
public function testSendWelcomeEmail() {
// 1. Создаем пророчество для MailerInterface
$mailerProphecy = $this->prophesize(MailerInterface::class);
// 2. Объявляем ожидание: метод send будет вызван один раз с определенными аргументами
$mailerProphecy->send('user@example.com', 'Welcome!', 'Thank you for joining!')
->willReturn(true);
// 3. Получаем mock-объект из пророчества
$mailerMock = $mailerProphecy->reveal();
// 4. Создаем тестируемый объект с mock-зависимостью
$userMailer = new UserMailer($mailerMock);
// 5. Выполняем тестируемый метод
$result = $userMailer->sendWelcomeEmail('user@example.com');
// 6. Проверяем результат
$this->assertTrue($result);
// 7. Prophecy автоматически проверяет, что все ожидания выполнены
}
}
Этот пример демонстрирует основной паттерн использования Prophecy: создание пророчества, объявление ожиданий, получение mock-объекта и его использование.
4. Основные методы/функции/классы
4.1. prophesize(string $classOrInterface): ObjectProphecy
Что делает: Создает новое пророчество для указанного класса или интерфейса. Это отправная точка для всех операций с Prophecy. Возвращает объект ObjectProphecy, который позволяет настраивать поведение и ожидания.
$prophecy = $this->prophesize(SomeClass::class);
// или напрямую (если не используете ProphecyTrait):
$prophecy = new Prophecy\Prophet();
$objectProphecy = $prophecy->prophesize(SomeClass::class);
4.2. willReturn(mixed $value): MethodProphecy
Что делает: Указывает, что метод должен вернуть заданное значение при вызове. Это самый простой способ задать возвращаемое значение для mock-метода.
$prophecy->getUserName(42)->willReturn('John Doe');
// При вызове $mock->getUserName(42) вернет 'John Doe'
4.3. willThrowException(\Throwable $exception): MethodProphecy
Что делает: Указывает, что метод должен выбросить исключение при вызове. Полезно для тестирования обработки ошибок.
$prophecy->save($data)->willThrowException(new \RuntimeException('Database error'));
// При вызове $mock->save($data) будет выброшено RuntimeException
4.4. will(callable $callback): MethodProphecy
Что делает: Позволяет задать произвольную логику выполнения метода через callback-функцию. Callback получает те же аргументы, что и оригинальный метод.
$prophecy->calculate($a, $b)->will(function($args) {
return $args[0] + $args[1] * 2;
});
// При вызове $mock->calculate(3, 4) вернет 11 (3 + 4*2)
4.5. shouldBeCalled() / shouldNotBeCalled(): MethodProphecy
Что делает: shouldBeCalled() указывает, что метод должен быть вызван хотя бы один раз. shouldNotBeCalled() — что метод не должен вызываться вообще. Если ожидания не оправдываются, Prophecy выбрасывает исключение при завершении теста.
$prophecy->sendEmail($email)->shouldBeCalled();
$prophecy->deleteUser($id)->shouldNotBeCalled();
// Prophecy проверит эти ожидания в конце теста
4.6. shouldBeCalledTimes(int $times): MethodProphecy
Что делает: Указывает точное количество раз, которое метод должен быть вызван. Позволяет более точно контролировать ожидания.
$prophecy->log($message)->shouldBeCalledTimes(3);
// Метод должен быть вызван ровно 3 раза
4.7. reveal(): object
Что делает: Преобразует пророчество в реальный mock-объект, который можно передавать в тестируемый код. После вызова reveal() пророчество считается "замороженным" — его нельзя изменить.
$mock = $prophecy->reveal();
$service = new SomeService($mock); // Передаем mock в тестируемый сервис
4.8. checkPredictions()
Что делает: Проверяет все объявленные ожидания (predictions) для пророчества. Обычно вызывается автоматически в tearDown() при использовании ProphecyTrait, но может быть вызван вручную для более тонкого контроля.
$prophecy->checkPredictions();
// Выбросит исключение, если ожидания не выполнены
4.9. withArguments(array $args): MethodProphecy
Что делает: Позволяет задать точные аргументы, с которыми ожидается вызов метода. Это альтернатива указанию аргументов непосредственно в цепочке вызовов.
$prophecy->findUser()->withArguments([42])->willReturn($user);
// Эквивалентно: $prophecy->findUser(42)->willReturn($user)
4.10. Argument::type(string $type)
Что делает: Статический метод класса Argument, который позволяет указать, что аргумент должен быть определенного типа (класс, интерфейс, скалярный тип). Полезно, когда точное значение аргумента не важно.
use Prophecy\Argument;
$prophecy->process(Argument::type(SomeInterface::class))->willReturn(true);
// Метод будет ожидать любой объект, реализующий SomeInterface
5. Реальный практический пример
Рассмотрим более сложный пример: тестирование сервиса заказов, который зависит от репозитория, API платежей и сервиса уведомлений.
<?php
// Тестируемые классы
interface OrderRepositoryInterface {
public function save(Order $order): void;
public function findById(int $id): ?Order;
}
interface PaymentGatewayInterface {
public function charge(float $amount, string $token): PaymentResult;
}
interface NotificationServiceInterface {
public function sendOrderConfirmation(Order $order): void;
}
class Order {
public int $id;
public float $total;
public string $status = 'pending';
public string $paymentToken;
}
class PaymentResult {
public bool $success;
public string $transactionId;
}
class OrderService {
private OrderRepositoryInterface $orderRepo;
private PaymentGatewayInterface $paymentGateway;
private NotificationServiceInterface $notifier;
public function __construct(
OrderRepositoryInterface $orderRepo,
PaymentGatewayInterface $paymentGateway,
NotificationServiceInterface $notifier
) {
$this->orderRepo = $orderRepo;
$this->paymentGateway = $paymentGateway;
$this->notifier = $notifier;
}
public function processOrder(int $orderId): bool {
$order = $this->orderRepo->findById($orderId);
if (!$order || $order->status !== 'pending') {
return false;
}
$paymentResult = $this->paymentGateway->charge($order->total, $order->paymentToken);
if ($paymentResult->success) {
$order->status = 'paid';
$order->transactionId = $paymentResult->transactionId;
$this->orderRepo->save($order);
$this->notifier->sendOrderConfirmation($order);
return true;
}
$order->status = 'failed';
$this->orderRepo->save($order);
return false;
}
}
// Тест с использованием Prophecy
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Argument;
class OrderServiceTest extends TestCase {
use ProphecyTrait;
public function testSuccessfulOrderProcessing(): void {
// Создаем пророчества для всех зависимостей
$orderRepoProphecy = $this->prophesize(OrderRepositoryInterface::class);
$paymentGatewayProphecy = $this->prophesize(PaymentGatewayInterface::class);
$notifierProphecy = $this->prophesize(NotificationServiceInterface::class);
// Создаем тестовый заказ
$order = new Order();
$order->id = 123;
$order->total = 99.99;
$order->status = 'pending';
$order->paymentToken = 'tok_test_123';
// Настраиваем ожидания для репозитория
$orderRepoProphecy->findById(123)
->shouldBeCalled()
->willReturn($order);
// Репозиторий должен сохранить заказ с обновленным статусом
$orderRepoProphecy->save(Argument::that(function(Order $savedOrder) {
return $savedOrder->status === 'paid'
&& $savedOrder->transactionId === 'txn_abc_456';
}))->shouldBeCalled();
// Настраиваем платежный шлюз
$paymentResult = new PaymentResult();
$paymentResult->success = true;
$paymentResult->transactionId = 'txn_abc_456';
$paymentGatewayProphecy->charge(99.99, 'tok_test_123')
->shouldBeCalled()
->willReturn($paymentResult); Также в библиотеке
Планировщик задач Java: Quartz Scheduler — полное руководство
WebSocket библиотеки для Node.js: ws, Socket.IO, uWS — полный обзор
Работа с Kafka в Rust: библиотеки и примеры для начинающих