Отличный выбор!
Сейчас разберём второй уровень по PHPUnit — не просто "проверить, что метод работает", а проверить взаимодействие между объектами через моки.

Это очень важный навык для middle+, потому что:

  • Ты проверяешь не только результат, но и процесс.
  • Такой подход покрывает именно бизнес-логику.
  • Это то, что любят спрашивать на собеседованиях: «А как бы вы проверили, что метод был вызван?»

🧩 Тестирование взаимодействий: моки в PHPUnit

📖 Сценарий

У нас есть такой сервис:

class TaskService {    public function __construct(        private TaskRepository $taskRepository,        private Logger $logger    ) {}     public function create(TaskDTO $dto): int    {        $task = new Task(            $dto->title,            $dto->description,            $dto->priority,            $dto->assigneeId,            new DateTimeImmutable()        );         $id = $this->taskRepository->save($task);         $this->logger->info('Task created', ['id' => $id]);         return $id;    } } 

🎯 Что мы хотим проверить:

  • ✅ Метод save() в TaskRepository вызывается один раз.
  • ✅ Метод info() в Logger вызывается с нужным сообщением и данными.
  • ✅ Можно проверить, что метод возвращает ID (чисто для полноты).

🛠️ Как пишется тест с моками

use PHPUnit\Framework\TestCase;  class TaskServiceTest extends TestCase {    public function testCreateTaskShouldSaveTaskAndLogMessage(): void    {        // Arrange        $dto = new TaskDTO(            'Test Task',            'Description',            Priority::from('high'),            1        );         // Мокаем TaskRepository        $repositoryMock = $this->createMock(TaskRepository::class);        $repositoryMock->expects($this->once())            ->method('save')            ->with($this->isInstanceOf(Task::class))            ->willReturn(42); // вернём ID         // Мокаем Logger        $loggerMock = $this->createMock(Logger::class);        $loggerMock->expects($this->once())            ->method('info')            ->with(                'Task created',                ['id' => 42]            );         // Act        $service = new TaskService($repositoryMock, $loggerMock);        $result = $service->create($dto);         // Assert        $this->assertEquals(42, $result);    } } 

📦 Объяснения по строчкам

  • $this->createMock(...);
    → создаём мок объекта.
  • expects($this->once())
    → проверяем, что метод вызван ровно один раз.
  • method('save')
    → указываем, какой метод мока отслеживать.
  • with($this->isInstanceOf(Task::class))
    → проверяем, что в метод передаётся именно объект класса Task.
  • willReturn(42)
    → подменяем поведение мока, чтобы метод вернул 42.
  • Отдельно loggerMock: → проверяем, что в лог передаётся именно то сообщение и именно те данные, которые мы ожидаем.

✅ Что мы проверили:

✔️ Поведение: метод save() репозитория вызывается как положено.
✔️ Побочный эффект: метод info() логгера вызывается с правильными параметрами.
✔️ Результат метода: метод возвращает ID 42, который мы подставили в мок.

🎯 Как тебя спросят на собеседовании:

— Как вы тестируете, что внешний сервис (например, Logger или Repository) вызывается?
— Ответ: "Я использую моки в PHPUnit и через метод expects() проверяю количество вызовов, а через with() — параметры вызова."

🧩 Мини чек-лист:

Как
expects($this->once())->method('methodName')
expects($this->never())->method('methodName')
with($this->equalTo(...)), with($this->isInstanceOf(...))
willReturn(value)
$this->once(), $this->exactly(n)

Если хочешь — могу дать практику: Я задам тебе схему класса с 2–3 зависимостями, а ты предложишь тест с моками для проверки взаимодействий.
Или идём к следующему уровню тестов — например, snapshot-тесты, data provider, или интеграционные.

Как тебе? 🚀