GrabDuck

Unit-тестирование в Codeception

:

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

Сегодня я хочу рассказать, как в 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