Неделю назад я уже писал о Codeception и об его использования для тестирования PHP приложений. После прошлого поста несколько багов было исправлено. Спасибо за багрепорты. Если вы ещё не пробовали Codeception, советую посмотреть прошлую статью и испытать его для приемочных тестов.
Сегодня я хочу рассказать, как в Codeception реализовано юнит-тестирование в BDD-стиле.
Замечу, что модуль для тестирования юнитов пока экспериментальный. Не в значении «нестабильный», а в значении «может и будет расширяться для удоволетворения всех необходимых нужд».
Прежде чем начать рассказывать о BDD-тестировании юнитов, я отвечу на вполне логичный вопрос, который у вас естественно возникнет: нафига козе баян? То есть, зачем нужны какие-то приблуды к юнит-тестам, если они и так отлично работают в том же PHPUnit'е. Зачем переписывать их в сценарной парадигме?
Все тесты должны быть читабельными. В особенности юнит-тесты, где тест по сути описывает функциональность метода. Но он бывает перегружен приготовлением среды, созданием стабов и моков. В нем бывает намешано много разных вызовов и порой напрочь отсутствуют комментарии. И новому человеку, сломавшему тест, бывает трудно вникнуть в то что же тут тестируется и как.
Codeception предлагает подход, где каждый шаг описывает выполняемое действие.
Вот например, так:
И зачем это нужно? Так отделяется исполняемый код от проверок. Впрочем, никто не запрещает использовать ассерты и внутри блока кода:
Чем сложнее становится тест, тем больше требуется для поддержания его читабельности.
Codeception становится интересен когда вам нужно протестировать классы бизнес-логики. Они обычно не существуют в изоляции, а в зависимости от других классов, и результат их выполнения порой не так просто проверить, как для геттера.
Возьмем вот такой простой контроллер из воображаемого MVC-фреймворка.
Что он делает, впринципе понятно. Показывает страницу профиля пользователя. Но тестировать его сложно, ведь прежде чем тестировать, нужно изолировать контроллер от View и Model. Вот как мы сделаем это в Codeception.
Как тот же тест я написал в PHPUnit можете посмотреть здесь. Получилось в 1,5 раза длиннее, и код, конечно, понятен, но если вы гуру PHPUnit.
Что хорошо в нашем коде: он имеет четкую структуру. Сначала мы создаем среду, дальше выполняем действия и проверяем результаты. Обратите внимание, мы проверяем был ли выполнен метод 'render' из контроллера после того, как выполнили наш метод 'show' с параметром 1. Таким образом, мы не смешиваем определение стабов с ассертами. Все проверки идут после выполнения тестируемого кода.
Насчет читабельности. Попробуем творчески перевести этот код в текст:
With this method I can render profile page for valid user
If I execute this method
I will see result equals: true
I will see method invoked: $controller, 'render'
I expect it will render page 404 for unexistent user
If I execute this method
I will see result not equals: true
I will see method invoked: $controller, 'render404'
I will see method not invoked: $controller, 'render'
Хоть бери и пиши в документацию. Возможно, вскоре генерация документации будет добавлена, но пока я просто хочу продемонстрировать, насколько четко и понятно сам код теста описывает тестируемый код.
Обратите внимание, как создаются стабы. Любой стаб делается одной командой. Например:
Это проще, чем то что предлагает PHPUnit. Вспомните хотя бы сколько параметров требует mockBuilder и что они все значат. Но что самое интересное, класс Stub это просто обертка над mockBuilder'ом. Заметьте, мы создаем только стабы, т.е. среду. А из них, динамически, той же командой seeMethodInvoked стаб превращаем в мок.
Больше информации в документации и в модуле Unit
Как я говорил вначале, эта штука экспериментальна, а значит, можно обсуждать. Но писалась она не для «сферического кода в вакууме», а исходя из своих же реальных потребностей. Впрочем, советую попробовать для своего проекта. Если какие-то моменты плохо освещены в документации — спрашивайте.
P.S. На оф сайте появилась статья про интеграцию Codeception и Zend Framework
Сегодня я хочу рассказать, как в Codeception реализовано юнит-тестирование в BDD-стиле.
Замечу, что модуль для тестирования юнитов пока экспериментальный. Не в значении «нестабильный», а в значении «может и будет расширяться для удоволетворения всех необходимых нужд».
Прежде чем начать рассказывать о BDD-тестировании юнитов, я отвечу на вполне логичный вопрос, который у вас естественно возникнет: нафига козе баян? То есть, зачем нужны какие-то приблуды к юнит-тестам, если они и так отлично работают в том же PHPUnit'е. Зачем переписывать их в сценарной парадигме?
Все тесты должны быть читабельными. В особенности юнит-тесты, где тест по сути описывает функциональность метода. Но он бывает перегружен приготовлением среды, созданием стабов и моков. В нем бывает намешано много разных вызовов и порой напрочь отсутствуют комментарии. И новому человеку, сломавшему тест, бывает трудно вникнуть в то что же тут тестируется и как.
Codeception предлагает подход, где каждый шаг описывает выполняемое действие.
Вот например, так:
<?php
class UserCest {
function setNameAndSave(CodeGuy $I)
{
$I->wantToTest('getter and setter of User model');
$I->execute(function () {
$user = new Model\User;
$user->setName('davert');
$user->save();
});
$I->seeInDatabase('users',array('name' => 'davert');
}
}
?>
И зачем это нужно? Так отделяется исполняемый код от проверок. Впрочем, никто не запрещает использовать ассерты и внутри блока кода:
<?php
$I->wantToTest('getter and setter of User model');
$I->execute(function () {
$user = new Model\User;
$user->setName('davert');
assertEquals('davert', $user->getName());
$user->save();
});
$I->seeInDatabase('users',array('name' => 'davert');
Чем сложнее становится тест, тем больше требуется для поддержания его читабельности.
Codeception становится интересен когда вам нужно протестировать классы бизнес-логики. Они обычно не существуют в изоляции, а в зависимости от других классов, и результат их выполнения порой не так просто проверить, как для геттера.
Возьмем вот такой простой контроллер из воображаемого MVC-фреймворка.
<?php
class UserController extends AbtractController {
public function show($id)
{
$user = $this->db->find('users',$id);
if (!$user) return $this->render404('User not found');
$this->render('show.html.php', array('user' => $user));
return true;
}
}
?>
Что он делает, впринципе понятно. Показывает страницу профиля пользователя. Но тестировать его сложно, ведь прежде чем тестировать, нужно изолировать контроллер от View и Model. Вот как мы сделаем это в Codeception.
<?php
class UserControllerCest {
public $class = 'UserController';
public function show(CodeGuy $I) {
$I->haveStub($controller = Stub::makeEmptyExcept($this->class, 'show'))
->haveStub($db = Stub::make('DbConnector', array(
'find' => function($id) { return $id ? new User() : null )))
->setProperty($controller, 'db', $db);
$I->wantTo('render profile page for valid user')
->executeTestedMethodOn($controller, 1)
->seeResultEquals(true)
->seeMethodInvoked($controller, 'render');
$I->expect('it will render page 404 for unexistent user')
->executeTestedMethodOn($controller, 0)
->seeResultNotEquals(true)
->seeMethodInvoked($controller, 'render404','User not found')
->seeMethodNotInvoked($controller, 'render');
}
}
Как тот же тест я написал в PHPUnit можете посмотреть здесь. Получилось в 1,5 раза длиннее, и код, конечно, понятен, но если вы гуру PHPUnit.
Что хорошо в нашем коде: он имеет четкую структуру. Сначала мы создаем среду, дальше выполняем действия и проверяем результаты. Обратите внимание, мы проверяем был ли выполнен метод 'render' из контроллера после того, как выполнили наш метод 'show' с параметром 1. Таким образом, мы не смешиваем определение стабов с ассертами. Все проверки идут после выполнения тестируемого кода.
Насчет читабельности. Попробуем творчески перевести этот код в текст:
With this method I can render profile page for valid user
If I execute this method
I will see result equals: true
I will see method invoked: $controller, 'render'
I expect it will render page 404 for unexistent user
If I execute this method
I will see result not equals: true
I will see method invoked: $controller, 'render404'
I will see method not invoked: $controller, 'render'
Хоть бери и пиши в документацию. Возможно, вскоре генерация документации будет добавлена, но пока я просто хочу продемонстрировать, насколько четко и понятно сам код теста описывает тестируемый код.
Обратите внимание, как создаются стабы. Любой стаб делается одной командой. Например:
<?php
// создаем простой класс с переопределенным методом save
$user = Stub::make('User', array('save' => function () {}));
// создаем пустой класс с указанными свойствами
$user = Stub::makeEmpty('User', array('name' => 'davert'));
// создаем пустой класс при помощи конструктора
$user = Stub::constructEmpty('Template', array('show.html', 'html'));
?>
Это проще, чем то что предлагает PHPUnit. Вспомните хотя бы сколько параметров требует mockBuilder и что они все значат. Но что самое интересное, класс Stub это просто обертка над mockBuilder'ом. Заметьте, мы создаем только стабы, т.е. среду. А из них, динамически, той же командой seeMethodInvoked стаб превращаем в мок.
Больше информации в документации и в модуле Unit
Как я говорил вначале, эта штука экспериментальна, а значит, можно обсуждать. Но писалась она не для «сферического кода в вакууме», а исходя из своих же реальных потребностей. Впрочем, советую попробовать для своего проекта. Если какие-то моменты плохо освещены в документации — спрашивайте.
P.S. На оф сайте появилась статья про интеграцию Codeception и Zend Framework