Вы когда-нибудь ловили себя на мысли, что после очередного рефакторинга боитесь запускать приложение? Знакомо чувство, когда правишь одну строчку, а ломается половина сайта? Тесты — это не бюрократия, а страховка. В этой статье мы разберём, как настроить PHPUnit для Yii-приложения и писать тесты так, чтобы они приносили пользу, а не головную боль.

Digital-студия WNDER

Почему Yii и PHPUnit — это не страшно

Yii — фреймворк, который из коробки дружит с тестированием. Он предоставляет специальные классы для unit- и функциональных тестов, работу с фикстурами и даже эмуляцию браузера. PHPUnit — стандартный инструмент для тестирования в PHP-мире. Вместе они дают мощный тандем: вы пишете код, а тесты проверяют, что всё работает как надо.

Многие думают: «Зачем тратить время на тесты, если можно просто проверить вручную?» Но ручная проверка — это как искать иголку в стоге сена. Один раз проверил, второй, третий... а на десятый забыл. Тесты же делают это за вас. И да, они экономят время в долгосрочной перспективе.

Правило: Начинайте с малого. Не нужно покрывать тестами всё приложение сразу. Напишите тест для одной критичной функции — и вы уже сделали шаг к стабильности.

Настройка PHPUnit в Yii

Если вы используете шаблон yii2-app-basic или yii2-app-advanced, PHPUnit уже включён в зависимости. Если нет — установите его через Composer:

composer require --dev phpunit/phpunit ^9.5

Для Yii 2.0 актуальна версия PHPUnit 9.5 (для PHP 7.4+) или 10 (для PHP 8.1+). Проверьте совместимость с вашей версией PHP.

Создайте файл конфигурации phpunit.xml в корне проекта. Пример для базового приложения:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
    <testsuites>
        <testsuite name="Unit Tests">
            <directory>tests/unit</directory>
        </testsuite>
        <testsuite name="Functional Tests">
            <directory>tests/functional</directory>
        </testsuite>
    </testsuites>
</phpunit>

Теперь создайте папки tests/unit и tests/functional. В каждой из них будет свой TestCase, унаследованный от Yii-классов: yii\codeception\Unit (если используете Codeception) или yii\test\UnitTest (для чистого PHPUnit).

Для чистого PHPUnit (без Codeception) Yii предоставляет базовые классы yii\test\UnitTest и yii\test\FunctionalTest. Они уже настроены на работу с приложением.

Пишем первый unit-тест

Unit-тесты проверяют отдельные классы и методы в изоляции. Допустим, у нас есть модель User с методом getFullName(). Хотим убедиться, что он возвращает правильную строку.

<?php

namespace tests\unit\models;

use app\models\User;
use yii\test\UnitTest;

class UserTest extends UnitTest
{
    public function testGetFullName()
    {
        $user = new User();
        $user->first_name = 'Иван';
        $user->last_name = 'Петров';

        $this->assertEquals('Иван Петров', $user->getFullName());
    }
}

Запускаем тест из командной строки:

./vendor/bin/phpunit tests/unit/models/UserTest.php

Если всё настроено верно, увидите зелёную надпись «OK». Если красную — не расстраивайтесь, это повод разобраться в коде.

Лайфхак: Используйте аннотацию @dataProvider для тестирования одного метода с разными входными данными. Это избавит от дублирования кода.

Функциональные тесты: проверяем работу приложения

Функциональные тесты имитируют действия пользователя: отправку форм, переходы по ссылкам, проверку ответов сервера. В Yii для этого есть класс FunctionalTest, который использует встроенный HTTP-клиент.

Пример теста для проверки страницы входа:

<?php

namespace tests\functional;

use yii\test\FunctionalTest;

class LoginCest extends FunctionalTest
{
    public function testLoginPageOpens()
    {
        $this->get('site/login');
        $this->assertResponseOk();
    }

    public function testLoginWithWrongCredentials()
    {
        $this->post('site/login', [
            'LoginForm[username]' => 'wrong',
            'LoginForm[password]' => 'wrong',
        ]);
        $this->assertFalse(\Yii::$app->user->isGuest);
    }
}

Обратите внимание: функциональные тесты требуют настроенного приложения. Убедитесь, что в конфигурации тестового окружения (config/test.php) указана тестовая база данных, чтобы не портить рабочие данные.

Фикстуры: подготавливаем данные для тестов

Фикстуры — это заранее подготовленные данные, которые загружаются в базу перед тестом и удаляются после. В Yii они реализованы через класс yii\test\Fixture.

Создадим фикстуру для таблицы user:

<?php

namespace tests\fixtures;

use yii\test\ActiveFixture;

class UserFixture extends ActiveFixture
{
    public $modelClass = 'app\models\User';

    public $dataFile = '@tests/fixtures/data/user.php';
}

Файл с данными tests/fixtures/data/user.php:

<?php

return [
    'user1' => [
        'id' => 1,
        'username' => 'admin',
        'email' => 'admin@example.com',
    ],
    'user2' => [
        'id' => 2,
        'username' => 'user',
        'email' => 'user@example.com',
    ],
];

Теперь в тесте можно загрузить фикстуру и использовать данные:

public function fixtures()
{
    return [
        'user' => UserFixture::class,
    ];
}

public function testFindUser()
{
    $user = User::findOne(1);
    $this->assertEquals('admin', $user->username);
}

Фикстуры автоматически очищают таблицы после теста, так что данные не будут накапливаться.

Моки и стабы: изолируем зависимости

Иногда нужно протестировать класс, который зависит от внешних сервисов (API, базы данных). Чтобы не зависеть от них, используйте моки (mock objects). PHPUnit предоставляет удобный API для создания заглушек.

Пример: класс PaymentService зависит от PaymentGatewayInterface. Мы хотим проверить, что при успешной оплате вызывается метод logPayment.

public function testProcessPaymentLogsSuccess()
{
    $gateway = $this->createMock(PaymentGatewayInterface::class);
    $gateway->method('charge')
        ->willReturn(true);

    $logger = $this->createMock(LoggerInterface::class);
    $logger->expects($this->once())
        ->method('logPayment');

    $service = new PaymentService($gateway, $logger);
    $service->process(100);
}

Моки позволяют сфокусироваться на логике тестируемого класса, не отвлекаясь на внешние факторы.

Совет: Не злоупотребляйте моками. Если тест состоит из одних заглушек, возможно, вы тестируете не то, что нужно.

Правила хорошего тона при написании тестов

Чтобы тесты приносили пользу, а не превращались в обузу, придерживайтесь нескольких принципов:

  • Один тест — одна проверка. Если тест упал, вы сразу поймёте, что именно сломалось.
  • Тесты должны быть быстрыми. Долгие тесты никто не будет запускать.
  • Изолируйте тесты. Они не должны зависеть от порядка выполнения.
  • Используйте понятные имена. testGetFullNameReturnsCorrectString лучше, чем testName.

И помните: тесты — это тоже код. Их нужно поддерживать и рефакторить.

Сравнение инструментов тестирования в Yii

В Yii можно тестировать по-разному. Вот таблица для сравнения подходов:

Инструмент Тип тестов Сложность настройки Скорость выполнения
PHPUnit + yii\test\UnitTest Unit Низкая Высокая
PHPUnit + yii\test\FunctionalTest Функциональные Средняя Средняя
Codeception Все виды Высокая Зависит от тестов

Для большинства проектов достаточно чистого PHPUnit с Yii-обёртками. Codeception стоит подключать, если нужны приёмочные тесты (acceptance) с эмуляцией браузера.

Что в итоге

Тестирование в Yii с PHPUnit — это не так сложно, как кажется. Начните с unit-тестов для критичных методов, затем добавьте функциональные для основных сценариев. Используйте фикстуры для подготовки данных и моки для изоляции. Постепенно вы построите надёжную защиту от багов, которая сэкономит вам дни отладки.

Главное — не пытайтесь покрыть тестами всё сразу. Двигайтесь маленькими шагами, и результат не заставит себя ждать.

Студия WNDER