Тестирование в Yii 2 с Codeception: рабочие приёмы
Как писать unit-, functional- и acceptance-тесты в Yii 2 через Codeception: где очищать базу между тестами, чем фикстуры лучше моков и почему functional-тесты падают там, где unit проходят.

Codeception — тестовый фреймворк, который идёт в базовой поставке Yii 2 (шаблоны yii2-app-basic и yii2-app-advanced). Он делит тесты на три уровня — unit, functional и acceptance — и для каждого даёт свой набор модулей. Проблема начинается, когда команда пишет всё как unit-тесты, ловит непонятные падения на данных из соседнего теста и решает, что тестировать Yii «слишком сложно». Разберём, как разложить тесты по уровням, где очищать базу и почему functional-тест видит запрос иначе, чем боевой сервер.
Статья про Yii 2 (2.0.4x) и Codeception 5.x. Инвалидация кеша, тестирование очередей и покрытие — отдельные темы, здесь только три уровня тестов и работа с базой.
Три уровня и чем они реально отличаются
Разница не в синтаксисе, а в том, сколько кода Yii участвует в тесте и что вы платите за скорость.
| Уровень | Что запускается | Модуль | Цена |
|---|---|---|---|
| unit | отдельный класс, БД по желанию | Yii2, Asserts | быстро, но не ловит проблемы маршрутизации и фильтров |
| functional | приложение целиком без веб-сервера, запрос эмулируется внутри PHP | Yii2 (part: init, orm, fixtures) | средне; поведение чуть отличается от реального HTTP |
| acceptance | реальный HTTP через WebDriver или PhpBrowser | WebDriver / PhpBrowser | медленно, нужен запущенный сервер или Selenium |
Практическое правило: логику модели и сервисов — в unit, поведение контроллеров и валидацию форм — в functional, JS и сквозные сценарии — в acceptance. Не наоборот.
Functional-тест не поднимает веб-сервер. Он вызывает приложение в том же процессе PHP, эмулируя запрос. Поэтому там не проверить настоящие редиректы браузера, работу сессионных кук через несколько доменов и всё, что зависит от реального заголовка HTTP.
Unit-тест модели: где очищать базу
Самая частая ошибка новичков в Yii-тестах — тест проходит в одиночку и падает в общем прогоне. Причина почти всегда в том, что данные не откатываются между тестами.
В Yii 2 порядок такой: подключаете модуль Yii2 с частью fixtures, объявляете фикстуры в методе _fixtures(), а Codeception грузит их перед тестом и выгружает после. Данные не «остаются».
<?php
// tests/unit/models/UserTest.php
// Codeception 5.x, Yii 2.0.4x
namespace tests\unit\models;
use app\models\User;
use tests\fixtures\UserFixture;
class UserTest extends \Codeception\Test\Unit
{
// фикстуры грузятся перед каждым тестом и откатываются после
public function _fixtures(): array
{
return [
'user' => [
'class' => UserFixture::class,
'dataFile' => codecept_data_dir() . 'user.php',
],
];
}
public function testFindByUsername(): void
{
$user = User::findByUsername('admin');
$this->assertInstanceOf(User::class, $user);
$this->assertSame('admin', $user->username);
}
public function testValidatePassword(): void
{
$user = User::findByUsername('admin');
// пароль из фикстуры
$this->assertTrue($user->validatePassword('secret'));
$this->assertFalse($user->validatePassword('wrong'));
}
}
Сам класс фикстуры — обычный ActiveFixture, привязанный к модели:
<?php
// tests/fixtures/UserFixture.php
namespace tests\fixtures;
use yii\test\ActiveFixture;
class UserFixture extends ActiveFixture
{
public $modelClass = 'app\models\User';
}
Файл данных — обычный массив строк. Пароль храните уже захешированным, как в боевой таблице, иначе validatePassword будет вести себя не так, как в проде.
<?php
// tests/_data/user.php
return [
'admin' => [
'id' => 1,
'username' => 'admin',
// здесь должен быть реальный хеш от Yii::$app->security->generatePasswordHash('secret')
'password_hash' => 'ЗАМЕНИТЕ_НА_РЕАЛЬНЫЙ_ХЕШ',
'auth_key' => 'test100key',
'email' => 'admin@example.com',
'status' => 10,
],
];
Чтобы фикстуры реально откатывали данные, модуль Yii2 в codeception.yml (или в конфиге unit-сьюта) должен включать часть fixtures и, для транзакционного отката, часть orm. Точный набор частей и их имена сверяйте с разделом Modules → Yii2 в документации Codeception для вашей версии — набор менялся между мажорными релизами.
Functional-тест контроллера через тестера
На functional-уровне вы работаете не с классом напрямую, а через объект-тестер (FunctionalTester), который «ходит» по приложению. Это ближе к тому, как ведёт себя пользователь, но без сети.
<?php
// tests/functional/LoginCest.php
namespace tests\functional;
use tests\FunctionalTester;
use tests\fixtures\UserFixture;
class LoginCest
{
public function _before(FunctionalTester $I): void
{
$I->haveFixtures([
'user' => [
'class' => UserFixture::class,
'dataFile' => codecept_data_dir() . 'user.php',
],
]);
}
public function loginWithValidCredentials(FunctionalTester $I): void
{
$I->amOnRoute('site/login');
$I->fillField('Username', 'admin');
$I->fillField('Password', 'secret');
$I->click('login-button');
// после логина форма логина исчезает
$I->dontSeeElement('#login-form');
}
public function loginWithWrongPassword(FunctionalTester $I): void
{
$I->amOnRoute('site/login');
$I->fillField('Username', 'admin');
$I->fillField('Password', 'wrong');
$I->click('login-button');
// Yii отрисует ошибку валидации в форме
$I->see('Incorrect username or password.');
}
}
Метод amOnRoute — из модуля Yii2, он избавляет от завязки на конкретный формат URL. Селектор кнопки 'login-button' должен совпадать с name или id в вашем шаблоне формы — если не совпадёт, тест не найдёт кнопку и упадёт с невнятной ошибкой.
Когда что выбирать
- Метод модели, валидатор, расчёт — unit. Не гоняйте это через контроллер: медленно и лишний шум в трейсе при падении.
- Проверка формы, редирект после сохранения, доступ по ролям — functional. Здесь важно, что отработали фильтры и behaviors.
- Клик по кнопке, которая работает на JS, оплата, drag-and-drop — acceptance с WebDriver. Всё остальное на этом уровне — перерасход времени прогона.
Соотношение простое: unit-тестов должно быть много и они быстрые, acceptance — мало и по критичным сценариям. Если у вас всё в acceptance, прогон CI будет идти минутами и станет тормозом.
Что ломается
- Данные текут между тестами. Забыли объявить фикстуры или не включили откат — первый тест создаёт запись, второй её «видит». Лечится через
_fixtures()/haveFixtures()и транзакционный откат. - Тестовая база совпала с рабочей. Codeception запускает миграции и чистит таблицы. Отдельный конфиг БД для тестов (в Yii обычно
config/test_db.php) — обязателен, иначе рискуете снести данные разработки. - Кеш и время «залипают». Если модель кеширует что-то через
Yii::$app->cache, а между тестами кеш не сбрасывается, получите ложные проходы. Чистите кеш в_before. - Ошибка «unknown method see() on FunctionalTester». Значит, метод пришёл из модуля, который не подключён к functional-сьюту. Проверьте список модулей в
tests/functional.suite.yml. - Хеш пароля в фикстуре не проходит проверку. В файле данных лежит текст вместо хеша от
security->generatePasswordHash(). Сгенерируйте хеш заранее и вставьте его.
Что делать дальше
Соберите базовую пирамиду: покройте unit-тестами модели и сервисы, functional-тестами — ключевые формы и права доступа, оставьте два-три acceptance-сценария на самое дорогое (оплата, регистрация). Настройте отдельную тестовую БД и запуск vendor/bin/codecept run в CI. Следующий шаг — включить сбор покрытия (флаг сбора покрытия у Codeception есть, точный синтаксис и требования к драйверу смотрите в разделе Coverage документации вашей версии) и следить, чтобы новый код приходил с тестами, а не отдельным тикетом «написать тесты потом».
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Codeception Documentation — Yii2 module, Yii 2 Guide — Testing
Частые вопросы
У меня Codeception 4, а примеры под 5. Что изменится?
Тест проходит в одиночку, но падает в общем прогоне. Почему?
Codeception не сотрёт мои рабочие данные при запуске?
Чем functional отличается от acceptance, если оба «ходят по страницам»?
Нужны ли моки, если есть фикстуры?
Как запустить только один уровень тестов?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.