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

Digital-студия WNDER

Зачем вообще тестировать Yii-приложение

Yii — фреймворк, который сам по себе довольно строгий. Он заставляет вас следовать MVC, использовать ActiveRecord, валидаторы. Но даже самая аккуратная архитектура не спасает от человеческих ошибок. Тесты дают уверенность: вы меняете код, запускаете пару команд — и видите, что ничего не сломалось. Это как проверить, выключили ли утюг, перед выходом из дома. Только для кода.

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

Установка и настройка PHPUnit в Yii

Yii не требует PHPUnit из коробки, но в шаблоне advanced и basic он уже прописан в composer.json. Если вы используете свой проект, добавьте пакет:

composer require --dev phpunit/phpunit ^9.5

Для Yii 2.0 актуальны версии PHPUnit 8 или 9. Не берите PHPUnit 10, если не хотите возиться с несовместимостью. После установки создайте файл phpunit.xml в корне проекта. Пример для basic-шаблона:

<?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. В каждой — файл bootstrap.php, который подключает Yii-приложение. Для unit-тестов достаточно автозагрузчика, но если тестируете модели с БД, лучше поднять консольное приложение.

Правило: Всегда используйте отдельную базу данных для тестов. Не запускайте PHPUnit на продакшене или на локальной базе с реальными данными. Создайте тестовую схему и очищайте её перед каждым тестом.

Пишем первый юнит-тест для модели

Начнём с простого. У вас есть модель User с методом getFullName(). Хотим проверить, что он возвращает имя и фамилию через пробел.

namespace tests\unit\models;

use app\models\User;
use PHPUnit\Framework\TestCase;

class UserTest extends TestCase
{
    public function testGetFullName()
    {
        $user = new User();
        $user->firstName = 'Ivan';
        $user->lastName = 'Petrov';

        $this->assertEquals('Ivan Petrov', $user->getFullName());
    }
}

Запускаем: ./vendor/bin/phpunit tests/unit/models/UserTest.php. Если всё ок, увидите зелёную точку. Если нет — красную F и сообщение об ошибке. Просто, правда?

Но часто модели используют подключение к БД. Для таких тестов нужно настроить фикстуры и очистку. Yii предоставляет класс yii\codeception\fixtures\ActiveFixture, но с PHPUnit это работает через наследование от yii\test\TestCase (в Yii 2.0 есть пакет yiisoft/yii2-test). Установите его:

composer require --dev yiisoft/yii2-test

Теперь ваш тест-класс может наследоваться от yii\test\TestCase, который автоматически подключает Yii-приложение и предоставляет методы для работы с БД.

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

Юнит-тесты проверяют отдельные методы. Функциональные — весь запрос-ответ. В Yii для этого есть встроенный класс yii\test\WebTestCase (или FunctionalTester в Codeception, но мы же про PHPUnit).

Пример теста для действия actionIndex в контроллере SiteController:

namespace tests\functional;

use yii\test\WebTestCase;

class SiteControllerTest extends WebTestCase
{
    public function testIndexPageReturnsSuccess()
    {
        $this->get('site/index');
        $this->assertResponseCode(200);
        $this->assertSee('Welcome');
    }
}

Здесь $this->get() эмулирует GET-запрос, assertResponseCode проверяет HTTP-код, а assertSee ищет текст на странице. Всё как в реальном браузере, только без мышки.

Лайфхак: Если тест падает из-за CSRF-токена, отключите его для тестового окружения. В конфиге приложения добавьте 'enableCsrfValidation' => false для компонента request.

Работа с базой данных: фикстуры и транзакции

Самое больное место — тесты, которые пишут в БД. Чтобы не засорять базу, используйте транзакции. Yii\test\TestCase оборачивает каждый тест в транзакцию и откатывает её после завершения. Но есть нюанс: если тестируете код, который сам управляет транзакциями, откат может не сработать. Тогда используйте фикстуры.

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

namespace tests\fixtures;

use yii\test\Fixture;

class UserFixture extends Fixture
{
    public $dataFile = '@tests/fixtures/data/user.php';
    public $depends = [];
}

В файле user.php возвращаем массив с данными. Потом в тесте указываем:

public $fixtures = [
    'users' => UserFixture::class,
];

Перед тестом таблица user заполнится, после — очистится. Удобно и предсказуемо.

Вот таблица, которая поможет выбрать стратегию работы с данными:

Метод Когда использовать Плюсы Минусы
Транзакции Простые тесты без вложенных транзакций Быстро, автоматический откат Не работает с DDL и вложенными транзакциями
Фикстуры Сложные данные, несколько таблиц Гибко, можно переиспользовать Медленнее, нужно писать классы
Ручная очистка Экзотические случаи Полный контроль Легко забыть, данные остаются

Полезные советы и частые грабли

За годы работы с Yii и PHPUnit я собрал несколько правил, которые экономят нервы.

  • Не тестируйте фреймворк. Если вы проверяете, что Yii правильно сохраняет модель, — это пустая трата времени. Тестируйте свою логику: валидацию, вычисления, бизнес-правила.
  • Изолируйте тесты. Один тест — одна проверка. Если тест упал, вы должны сразу понять, что именно сломалось.
  • Используйте dataProvider для однотипных проверок. В PHPUnit есть аннотация @dataProvider. Она позволяет прогнать один тест с разными входными данными.

Пример dataProvider:

public function providerForFullName()
{
    return [
        ['Ivan', 'Petrov', 'Ivan Petrov'],
        ['', 'Petrov', 'Petrov'],
        ['Ivan', '', 'Ivan'],
    ];
}

/**
 * @dataProvider providerForFullName
 */
public function testGetFullNameWithDifferentData($first, $last, $expected)
{
    $user = new User();
    $user->firstName = $first;
    $user->lastName = $last;
    $this->assertEquals($expected, $user->getFullName());
}

Ещё один момент: не забывайте про setUp() и tearDown(). В setUp() можно создавать объекты, которые нужны всем тестам, а в tearDown() — чистить за собой. Но не злоупотребляйте: если объект нужен только одному тесту, создавайте его внутри метода.

Правило: Запускайте тесты перед каждым коммитом. Настройте pre-commit hook или просто возьмите в привычку выполнять ./vendor/bin/phpunit. Это дешевле, чем потом ловить баги в продакшене.

Что в итоге

Тесты для Yii с PHPUnit — это не страшно, а удобно. Вы один раз настраиваете phpunit.xml, пишете пару тестов для моделей, потом добавляете функциональные. Постепенно покрытие растёт, и вы начинаете спать спокойнее. Главное — не гнаться за 100% покрытием. Тестируйте критичные участки: авторизацию, платежи, отправку форм. Остальное — по желанию.

Помните: тесты — это инвестиция в будущее. Чем раньше начнёте, тем меньше боли при рефакторинге. Удачи в написании зелёных тестов!

Студия WNDER