Pull to refresh

PHP + BDD = Behat, или сказ о чудо-библиотеке

PHP *
Sandbox
Каждый, кто всерьез занимался разработкой на Ruby, знает про замечательный гем Cucumber. Вкратце — это библиотека для автоматизированного тестирования, заточенная под BDD. Подробнее можно почитать в топике хабраюзера dapi, а еще лучше посмотреть подкаст от Райна Бэйтса. Основная прелесть «огурца» состоит в том, что он позволяет писать тесты на понятном человеку языке, и даже не обязательно английском. Выглядит это так:

Feature: Addition 
  In order to avoid silly mistakes 
  As a math idiot 
  I want to be told the sum of two numbers 

  Scenario: Add two numbers 
    Given I have entered 50 into the calculator
      And I have entered 70 into the calculator
     When I press add
     Then The result should be 120 on the scree

Благодаря Cucumber я подсел на BDD на рельсах. Но вот на PHP, с которым приходится работать основную часть времени, отношения с BDD как-то не сложились. И в первую очередь из-за отсутствия достойного инструментария. Но однажды судьба завела меня на страницу библиотеки Behat (написанной, кстати говоря, хабраюзером everzet). И на меня свалилось счастье…

Установка и предварительная настройка


Собственно, Behat — это «Cucumber от мира PHP». Тот же синтаксис (используется язык Gherkin), та же структура файлов, практически идентичный метод определения шагов. Видно, что автор вдохновлялся «старшим братом» своей библиотеки, чего он и не скрывает. С виду все красиво, но не смотреть же мы на нее пришли. Пора испробовать продукт в деле.

Сразу оговорюсь — Behat для своей работы требует PHP 5.3.1. Поэтому, если вы еще не обзавелись оным, самое время это сделать.

Автор на сайте предлагает несколько путей для установки Behat. Я выберу самый первый и самый простой — через Pear:
$ pear channel-discover pear.everzet.com
$ pear install everzet/behat-beta

Тадам! Мы — счастливые обладатели Behat :) В принципе, можно уже хоть сейчас идти в папку с тестируемым проектом и начинать писать сценарии, но я предпочитаю немножко «протюнинговать» библиотеку. Итак, первым делом создадим в папке с проектом родную для Behat среду обитания. Она, как уже упоминалось, заимствована у Cucumber и имеет следующий вид:

|-- features
   `-- steps
   |   `-- *_steps.php
   `-- support
       `-- env.php


Подробнее о том, что мы имеем:
  • features — это папка, где у нас будут храниться сценарии (файлы *.feature, о них чуть позже);
  • features/steps — это папка с описаниями «шагов» тестирования;
  • features/support — папка со скриптами поддержки. Особую важность здесь имеет файл env.php, в котором описывается конфигурация среды. Его и откроем.

Изначально файл env.php пуст. Основная его важность для нас состоит в том, что он выполняется каждый раз перед выполнением очередного сценария. И именно здесь удобно подключить все нужные нам библиотеки и файлы проекта. Здесь же удобно определить переменные и функции, которые понадобятся нам в тестах. Для их хранения, кстати, очень удобно использовать переменную $world, которая подается нам на входе, каждый раз новая. По большому счету, для использования базового функционала нам нет никакой необходимости что-то настраивать, вполне хватит того, что предоставляет нам Behat и сам PHP. Но мне все же нравится использовать функции проверок из PHPUnit, и я их, с вашего позволения, подключу.

Если у вас не установлен PHPUnit, устанавливается он крайне просто, через тот же Pear:
$ pear channel-discover pear.phpunit.de
$ pear channel-discover components.ez.no
$ pear channel-discover pear.symfony-project.com
$ pear install phpunit/PHPUnit

Готово, осталось только прописать в env.php:
<?php
require_once 'PHPUnit/Autoload.php';
require_once 'PHPUnit/Framework/Assert/Functions.php';
?>

Так же, чтобы у нас были доступны классы и функции из нашего проекта, нужно бы подключить к среде соответствующие файлы. Я не люблю захламлять инициализацию среды посторонними включениями, а потому объединю их в один файл — includes.php, а его уже подключу к среде. В итоге файл env.php имеет вид:

<?php
require_once 'PHPUnit/Autoload.php';
require_once 'PHPUnit/Framework/Assert/Functions.php';
include 'includes.php';
?>

Описываем фичи и производим первый запуск


Ну, вот и все, мы готовы к тестированию, можно начинать описывать фичи. В качестве примера я решил взять такую сложную в реализации и тестировании вещь, как сложение двух чисел. Итак, создаем в папке features файл calc.feature и пишем:

Feature: Addition
    In order to avoid silly mistakes 
    As a math idiot 
    I want to be told the sum of two numbers

    Scenario:
       Given I have an calculator
        When I have entered 30 as first number
         And I have entered 20 as second number
         And I press 'Add'
        Then The result should be 50


Сохраняем, и вбиваем в консоль команду
$ behat features

На выходе получаем:
1 scenario (1 undefined)
5 steps (5 undefined)
0.091s

You can implement step definitions for undefined steps with these snippets:

$steps->Given('/^I have an calculator$/', function($world) {
    throw new \Everzet\Behat\Exception\Pending();
});

$steps->When('/^I have entered (\d+) as first number$/', function($world, $arg1) {
    throw new \Everzet\Behat\Exception\Pending();
});

$steps->And('/^I have entered (\d+) as second number$/', function($world, $arg1) {
    throw new \Everzet\Behat\Exception\Pending();
});

$steps->And('/^I press \'([^\']*)\'$/', function($world, $arg1) {
    throw new \Everzet\Behat\Exception\Pending();
});

$steps->Then('/^The result should be (\d+)$/', function($world, $arg1) {
    throw new \Everzet\Behat\Exception\Pending();
});

Это говорит нам о том, что у нас не определены «шаги» сценария. Но мы-то знаем еще больше — у нас нет класса калькулятора. Напишем, не проблема.

class Calc {
    protected $first = 0;
    protected $second = 0;
    protected $result = 0;

    public function setFirst($num){ $this->first = $num; }
    public function setSecond($num){ $this->second = $num; }
    public function add(){ $this->result = $this->first + $this->second; }
    public function getResult(){ return $this->result; }
}

Описываем шаги и проводим успешный тест


Ну, а теперь можем перейти к описанию шагов. Да, кстати, не забудьте подключить файл с классом калькулятора в файл features/support/includes.php. Как мы видим, Behat любезно предложил нам шаблоны определения шагов. Скопируем их, немного подправим и сохраним в файл features/steps/calc_steps.php. Должно получиться примерно следующее:
<?php
$steps->Given('/^I have an calculator$/', function($world) {
    $world->calc = new Calc();
});
$steps->When('/^I have entered (\d+) as first number$/', function($world, $num) {
    $world->calc->setFirst($num);
});
$steps->When('/^I have entered (\d+) as second number$/', function($world, $num) {
    $world->calc->setSecond($num);
});
$steps->When('/^I press \'Add\'$/', function($world) {
    $world->calc->add();
});
$steps->Then('/^The result should be (\d+)$/', function($world, $res) {
    assertEquals($res,$world->calc->getResult());
});
?>

Запускаем тест заново — вуаля! Тест пройден :)

Feature: Addition
  In order to avoid silly mistakes
  As a math idiot
  I want to be told the sum of two numbers

  Scenario:                                # features/calc.feature:6
    Given I have an calculator             # features/steps/calc_steps.php:5
    When I have entered 30 as first number # features/steps/calc_steps.php:9
    And I have entered 20 as second number # features/steps/calc_steps.php:13
    And I press 'Add'                      # features/steps/calc_steps.php:17
    Then The result should be 50           # features/steps/calc_steps.php:21

1 scenario (1 passed)
5 steps (5 passed)
0.114s

Пытаемся тестировать страницу


Тестирование работы классов — это, безусловно, важная часть проекта. Но заказчик по обыкновению хочет от нас не рабочие классы, а правильно (по его мнению) функционирующее приложение. Проще говоря — ему нужна правильная работа интерфейса. Проверкой чего мы сейчас и займемся. В Ruby on Rails для тестирования страниц традиционно используется связка cucumer+webrat+nokogiri. В мире PHP все оказалось несколько сложнее… Сам everzet на своей странице предлагает использовать для этого дела библиотеку Goutte, основанную на Symfony 2. Данная библиотека сильно уступает по возможностям вышеупомянутой связке, но лучше в интернетах ничего найти не удалось (если оное кому-то известно, отпишитесь, пожалуйста). Goutte устанавливается крайне просто — скачиваем phar-архив и подключаем его к env.php.

В качестве примера для тестирования я набросал простенькую страничку:
<!DOCTYPE html>
<html>
    <head>
    </head>
    <body>
        <div><?
            if ($_GET["submit"]) {
                echo "Text = "     . $_GET['textfield'] . "<br />";
                echo "Checkbox = " . $_GET['checkbox']  . "<br />";
                echo "Radio = "    . $_GET['radio']     . "<br />";
                echo "Select = "   . $_GET['selectbox'] . "<br />";
            }
        ?></div>
        <form method="get" action="behat.php">
            <div>
                <input type="text" name="textfield" value="">
            </div>
            <div>
                <input type="checkbox" name="checkbox" value="checkbox">
            </div>
            <div>
                <input type="radio" name="radio" value="radio1" checked="checked">
                <input type="radio" name="radio" value="radio2">
                <input type="radio" name="radio" value="radio3">
            </div>
            <div>
                <select name="selectbox">
                    <option value="option1" selected="selected">option1</option>
                    <option value="option2">option2</option>
                    <option value="option3">option3</option>
                </select>
            </div>
            <div>
                <input type="submit" name="submit" value="Submit">
            </div>
            <div id="linkdiv">
                <a href="behat.php?textfield=text&checkbox=checkbox&radio=radio3&selectbox=option2&submit=Submit">Click me</a>
            </div>
        </form>
    </body>
</html>

Задача: проверить работу формы на ней. Да, задача весьма синтетическая, но это просто пример :) Итак, накидаем перечень тестов:
Feature: tests
    After submit form all values of fields
    Should be showed in top of page

    Scenario: Fill field
        Given I'm on test page
         When I fill in 'textfield' with 'some text' in form 'Submit'
          And I submit form 'Submit'
         Then I should see 'Text = some text'

    Scenario: Checking checkbox
        Given I'm on test page
         When I tick checkbox 'checkbox' in form 'Submit'
          And I submit form 'Submit'
         Then I should see 'Checkbox = checkbox'

    Scenario: Selecting radio
        Given I'm on test page
         When I select 'radio2' in radio 'radio' in form 'Submit'
          And I submit form 'Submit'
         Then I should see 'Radio = radio2'
         Then I should not see 'Radio = radio1'

    Scenario: Selecting option in selectbox
        Given I'm on test page
         When I select 'option3' in selectbox 'selectbox' in form 'Submit'
          And I submit form 'Submit'
         Then I should see 'Select = option3'
         Then I should not see 'Select = option1'

    Scenario: Fill some fields
        Given I'm on test page
         When I fill in following in form 'Submit':
            | textfield | some text |
            | checkbox  | true      |
            | radio     | radio3    |
            | selectbox | option2   |
          And I submit form 'Submit'
         Then I should see 'Text = some text'
          And I should see 'Checkbox = checkbox'
          And I should see 'Radio = radio3'
          And I should see 'Select = option2' within 'div'

    Scenario: Clicking on link
        Given I'm on test page
         When I click on link 'Click me' within '#linkdiv'
         Then I should see 'Text = text'
          And I should see 'Checkbox = checkbox'
          And I should see 'Radio = radio3'
          And I should see 'Select = option2' within 'div'
          And the 'textfield' field in form 'Submit' should be blank


Небольшое пояснение, почему форма в тестах носит имя «Submit». В Goutte выбор формы осуществляется по кнопке сабмита на ней. Сама же кнопка ищется по id или value. А кнопка на тестовой странице носит гордое имя Submit, отсюда и такая формулировка.

Шагов много, шаги разные, и все замешаны на работе с Goutte. К счастью, все эти шаги уже описаны мною и собраны в файл. Стянуть искомое можно с git-репозитория git://github.com/DarthSim/behat_websteps.git. В репозитории вы найдете:
  • Файл features/support/env.php с уже настроенным для тестирования страниц окружением;
  • Файл features/support/paths.php для описания путей (подробности чуть ниже);
  • Файл features/support/includes.php для подключения ваших классов;
  • Файл features/support/goutte.phar — собственно Goutte;
  • Файл features/steps/custom.php с описаниями шагов для тестирования страниц.

Копируем файлы в папку с вашим проектом, сохраняя иерархию, и запускаем тест. И получаем ошибку…

Unknown path 'test page'. You can define it in [features_folder]/support/paths.php

Ошибка говорит сама за себя — тест не знает, что за «test page» мы хотим проверить. Исправить проще простого — прописать путь для страницы «test page» в файле features/support/paths.php. Предположим, что тестовая страница у нас размещена по адресу tests.dev/behat.php, значит в файле путей нам нужно прописать

$world->paths['test page'] = "http://tests.dev/behat.php";

Запускаем тест — отлично, тест пройден! Разбор остальных шагов я оставляю читателю, благо они похожи на аналоги из webrat. Конечно, полученный функционал много беднее, чем у webrat, но, думаю, это только начало, дальше будет лучше.

Делаем выводы и приводим ссылки


Итак, можно с уверенностью сказать, что Behat является достойным наследником Cucumber в мире PHP. Библиотека еще не доросла до версии 1.0, но уже представляет собой качественный, законченный продукт. Пожелаем разработчику успехов в развитии его детища и пошлем ему лучи добра. Ниже — некоторые полезные ссылки:

Репозиторий Behat на GitHub
Behat WIKI
Behat API
Репозиторий Goutte
Symfony 2 API (поможет в работе с Goutte)
Tags:
Hubs:
Total votes 39: ↑36 and ↓3 +33
Views 37K
Comments Comments 42