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
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 18

      0
      >seeMethodInvoked
      Правильно понимаю, что через xdebug отслеживается?
        0
        или debug_backtrace?
          0
          Нет. Гораздо меньше магии )
          Хотя идея хороша, подумаю о ней.
            +1
            Отвечу более полно: всё дело в том, что мы пишем сценарий. И выполняется он только когда уже написан полностью. А значит метод execute при выполнении может проследить кто и что проверяет после него, и создать моки. Как я говорил, моки обычные, из PHPUnit. В документации много времени посвящено как писать тесты в условиях такой вот парадигмы. Но вот мне показалось, что этот «баг» можно красиво превратить в «фичу» и сейчас понимаю, что она достаточно удобна.
          0
          можно ли как-то безболезненно «отвязать» от Symphony и привязать к другому фреймворку?! очень хотелось бы такую штуку на Yii, например
            0
            А я как бы тут ни разу про Symfony и не упоминал )

            Для юнит-тестов можете использовать хоть сейчас. Для функциональных — нужно написать интеграцию. Это как раз несложно, но мне нужно работающее приложение на Yii, на котором можно тестировать, и человек для консультаций. Вообщем, постучитесь в скайп davert.ua, если интересно разработать эту тему.
            0
            it will render page 404 for unexistent user это же произвольная строка, служащая по сути лишь для документирования?

            И ещё вопрос: в codecept run [acceptance|functional] acceptance/functional — это «термины» Codecept или просто названия каталогов? Просто сейчас интересуют интеграционные тесты прежде всего (например, что при вызове функции/метода в базу что-то запишется), но хотелось бы их отделить от будущих юнит и приемочных. От первых из-за скорости, от вторых из-за разной природы. То есть, грубо говоря, интеграционные — это те же юнит, но в которых не всё мокится/стабится. П крайней мере я так привык с PHPUnit :)

            P.S. Может заменить в вызовах executeTestedMethodOn($controller, 1) и executeTestedMethodOn($controller, 0) 1 и 0 на константы/переменные? А то несколько минут не мог понять для чего они (переработал с $this->at($index) в PHPUnit :) )
              +1
              it will render page 404 for unexistent user это же произвольная строка, служащая по сути лишь для документирования?


              Да, комментарий, введенный в код, чисто затем чтобы неочевидные варианты поведения стоит дополнительно описывать.

              но хотелось бы их отделить от будущих юнит и приемочных

              Да, собственно, потому изначально делается 3 тестовых сьюиты. Каждая из сьюит это конфигурационный файл в папке tests + каталог там же. Если 3х окажется мало, сделайте себе integration:

              — скопируйте конфиг unit.suite.yml > integration.suite.yml
              — создайте папку tests/integration
              — и важно не забыть в integration.suite.yml — указать нового «парня», т.е. Guy-class, например, InterGuy.
              — подключить модули и выполнить 'codecept build'.

              Впринципе очень даже спасибо за вопрос, как раз создание своих сьюит пока плохо освещено в документации.

              Может заменить в вызовах executeTestedMethodOn($controller, 1) и executeTestedMethodOn($controller, 0) 1 и 0 на константы/переменные?

              Тоже правильная идея :) Спасибо.
              0
              Как заставить работать Unit тесты с symfony 1.4?
                0
                Нужно как минимум инициализировать автолоадер symfony, ну или само приложение.

                В файле tests/unit/_bootstrap.php добавим такие строчки. По крайней мере так это работает у меня.

                require_once(dirname(__FILE__).'/../../config/ProjectConfiguration.class.php');
                sfProjectConfiguration::getApplicationConfiguration('frontend','test', true);
                


                Теперь классы symfony доступны в тестах.

                  0
                  Да, спасибо, уже сделал. Но после вот такие вот дела:
                  Couldn't (CommentCest.php:CommentCest::setNameAndSave)
                  ErrorException: ob_end_clean(): failed to delete buffer. No buffer to delete
                  FAILURES!
                  Tests: 1, Assertions: 0, Errors: 1.
                  session_start(): Cannot send session cookie — headers already sent
                    0
                    Скорее всего, нужно обновить версию. Последняя 1.0.2:

                    codeception.com/02-05-2012/minor-release-1-0-2.html
                      0
                      Только сегодня последнюю и поставил через PEAR.
                        0
                        Упс, надо было ENV test поставить, теперь все ок :)
                0
                Ребята, если вам нужны только unit-тесты, то мой вам совет: не используйте Codeception, т.к. он сильно ограничивает phpunit и как пример: вы просто не сможете воспользоваться phpunit.xml настройками, который описаны в документации к phpunit, еще пример: Codeception использует свой хендлер ошибок, полностью перекрывая PHPUnit_Util_ErrorHandler::handleError, и кроме как хака перекрывающего хендлер обратно, это Вы никак не решите. И таких случаев очень много. В настоящий момент, когда у меня в проекте уже естьCodeception, я не понимаю, зачем Codeception мне нужен для unit-тестов, и найти список плюсов, я увы нигде найти не смог.
                  0

                  вообще этот пост дико устарел, сейчас этого функционала просто нет.


                  "если вам нужны только юнит тесты" — это ключевая фраза. Соглаесн, для юнит-тестов ничего нового не придумаешь. Для небольших библиотек я тоже использую PHPUnit. А вот для всего остального скорее всего понадобятся ещё функциональные, интеграционные и другие тесты. И ковыряя всё это но PHPUnit'е быстро можно натворить херни.

                    0
                    А какой есть вместо него?
                    P.S. Просто в Яндексе по запросу "codeception stub" ссылка на эту статью на 3 позиции.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое