BDD в PHP
с использованием
   Behat и Mink
About me

•   Symfony developer at
    KnpLabs

•   twitter: @tyomo4ka

•   GitHub: tyomo4ka
Happy Awesome Developers
Agenda

• BDD
• Gherkin DSL
• Behat
• Mink
• BDD workflow
Зачем тестировать?

• Безопасный рефакторинг
• Отсутствие регрессий
• Более качеcтвенная архитектура
• Уменьшение числа багов
• Степень зрелости разработчика?
Behavior Driven
 Development
TDD

• TDD не очень удачое название
• Если мы пишем тесты перед кодом, мы
  все равно думаем об архитектуре
• Design Driven Development?
TDD и BDD
• Также пишем тесты перед кодом
• При TDD мы фиксируем в тестах
  архитектуру приложения или его частей
• При BDD мы фиксируем в тестах
  поведение приложения или его частей
• Описательная часть: спецификация или
  пользовательские сценарии
Spec BDD
• Добавляем описательную часть к тестам
• Получаем не тесты, а спецификации
  объектов
• Тестирование системы изнутри
• Используем вместо юнит тестов и
  интеграционых тестов
Story (Scenario) BDD
• Вместо тестов описываем шаги, которые
  нужно выполнить для достижения
  определнного результата
• Шаги должны легко читаться, в идеале
  это должны быть простые предложения
• Тестирование системы снаружи
• Замена функциональным тестам
Gherkin DSL
Feature: Customer login
    In order to view protected data
    As a customer
    I need to be able to login

    Background:
        Given customers are registered:
            | username         | password | blocked |
            | active@user.com | password | no       |
            | blocked@user.com | password | yes     |

    Scenario: Successful login
        Given I am on page "Login"
        When I fill in "Username" with "active@user.com"
        And I fill in "Password" with "password"
        And I press "Submit"
        Then I should be on page "Personal profile"
        And I should see "Successful login"
Feature: Название функционала
    In order to ... Ценность функционала
    As a ... Выгодополучатель
    I need ... Краткое описание функционала

    Background:
        Given ... Начальное состояние системы

    Scenario: Название сценария
        Given ... Начальное состояние
        And ... Начальное состояние
        When ... Выполняем шаг
        And ... Выполняем шаг
        Then ... Проверяем результат
        And ... Проверяем результат
Behat
Зачем?


•   A php framework for testing your
    business expectations
Установка

• PHP 5.3
• Composer
• PHAR
• Git
Инициализация

• behat --init
• features/ directory
• features/bootstrap/ directory
• features/bootstrap/*Context.php
• behat.yml
features/*.feature


• Gherkin DSL
• behat --story-syntax --lang=LANG
[Feature|Business Need|Ability]: Internal operations
  In order to stay secret
  As a secret organization
  We need to be able to erase past agents' memory

 Background:
   Given there is agent A
   And there is agent B

 Scenario: Erasing agent memory
   Given there is agent J
   And there is agent K
   When I erase agent K's memory
   Then there should be agent J
   But there should not be agent K

  [Scenario Outline|Scenario Template]: Erasing other agents' memory
    Given there is agent <agent1>
    And there is agent <agent2>
    When I erase agent <agent2>'s memory
    Then there should be agent <agent1>
    But there should not be agent <agent2>

    [Examples|Scenarios]:
      | agent1 | agent2 |
      | D      | M      |
Feature:	
  Listing	
  developers
	
  	
  As	
  a	
  Visitor
	
  	
  I	
  want	
  to	
  browse	
  through	
  developers	
  list

	
  	
  Background:
	
  	
  	
  	
  Given	
  the	
  site	
  has	
  following	
  users:
	
  	
  	
  	
  |	
  name	
  	
  	
  	
  	
  	
  |
	
  	
  	
  	
  |	
  knplabs	
  	
  	
  |
	
  	
  	
  	
  |	
  fos	
  	
  	
  	
  	
  	
  	
  |
	
  	
  	
  	
  Given	
  the	
  site	
  has	
  following	
  bundles:
	
  	
  	
  	
  |	
  username	
  	
  |	
  name	
  	
  	
  	
  	
  	
  	
  |	
  description	
  |	
  lastCommitAt	
  |	
  score	
  |	
  trend1	
  |
	
  	
  	
  	
  |	
  knplabs	
  	
  	
  |	
  TestBundle	
  |	
  test	
  desc	
  	
  	
  |-­‐1	
  day	
  	
  	
  	
  	
  	
  	
  	
  |	
  10	
  	
  	
  	
  |	
  15	
  	
  	
  	
  	
  |
	
  	
  	
  	
  |	
  fos	
  	
  	
  	
  	
  	
  	
  |	
  UserBundle	
  |	
  user	
  desc	
  	
  	
  |-­‐2	
  days	
  	
  	
  	
  	
  	
  	
  |	
  20	
  	
  	
  	
  |	
  5	
  	
  	
  	
  	
  	
  |

	
  	
  Scenario:	
  Listing	
  developers
	
  	
  	
  	
  When	
  I	
  go	
  to	
  "/"
	
  	
  	
  	
  And	
  I	
  follow	
  "Developers"
	
  	
  	
  	
  Then	
  I	
  should	
  see	
  "2	
  developers	
  using	
  Symfony2"
	
  	
  	
  	
  And	
  I	
  should	
  see	
  "knplabs"	
  developer
	
  	
  	
  	
  And	
  I	
  should	
  see	
  "fos"	
  developer
Context

• POPO
• Описание шагов
• Хуки
• Subcontexts
• Closures для описания шагов и хуков
 	
  	
  	
  public	
  function	
  __construct($kernel)
	
  	
  	
  	
  {
	
  	
  	
  	
  	
  	
  	
  	
  $this-­‐>useContext('symfony_doctrine',	
  new	
  SymfonyDoctrineContext());
	
  	
  	
  	
  	
  	
  	
  	
  $this-­‐>useContext('solr',	
  new	
  SolrContext());
	
  	
  	
  	
  	
  	
  	
  	
  $this-­‐>useContext('mink',	
  new	
  MinkContext());
	
  	
  	
  	
  	
  	
  	
  	
  $this-­‐>useContext('api',	
  new	
  ApiContext());
	
  	
  	
  	
  }
Steps definition

• @Given, @When, @Then
• Если шаг не выбросил исключение,
  значит он завершился успешно
• Нет своих асершенов, но легко можно
  использовать асершены из PHPUnit
 	
  	
  	
  /**
	
  	
  	
  	
  	
  *	
  @Given	
  /^the	
  bundles	
  have	
  following	
  keywords:$/
	
  	
  	
  	
  	
  */
	
  	
  	
  	
  public	
  function	
  theBundlesHaveFollowingKeywords(TableNode	
  $table)
	
  	
  	
  	
  {
	
  	
  	
  	
  	
  	
  	
  	
  $entityManager	
  =	
  $this-­‐>getEntityManager();

	
  	
  	
  	
  	
  	
  	
  	
  foreach	
  ($table-­‐>getHash()	
  as	
  $row)	
  {
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  if	
  (isset($this-­‐>bundles[$row['bundle']]))	
  {
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  $bundle	
  =	
  $this-­‐>bundles[$row['bundle']];
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  $keyword	
  =	
  $entityManager
                                                -­‐>getRepository('KnpBundleKnpBundlesBundleEntityKeyword')
                                                -­‐>findOrCreateOne($row['keyword']);
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  $bundle-­‐>addKeyword($keyword);
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  $entityManager-­‐>persist($bundle);
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  }
	
  	
  	
  	
  	
  	
  	
  	
  }

	
  	
  	
  	
  	
  	
  	
  	
  $entityManager-­‐>flush();
	
  	
  	
  	
  }
Hooks

• BeforeStep/AfterStep
• BeforeScenario/AfterScenario
• BeforeFeature/AfterFeature
• BeforeSuite/AfterSuite
• Hooks can be tagged
 	
  	
  	
  /**
	
  	
  	
  	
  	
  *	
  @BeforeScenario
	
  	
  	
  	
  	
  *
	
  	
  	
  	
  	
  *	
  @return	
  null
	
  	
  	
  	
  	
  */
	
  	
  	
  	
  public	
  function	
  buildSchema($event)
	
  	
  	
  	
  {
	
  	
  	
  	
  	
  	
  	
  	
  $metadata	
  =	
  $this-­‐>getMetadata();

	
  	
  	
  	
  	
  	
  	
  	
  if	
  (!empty($metadata))	
  {
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  $tool	
  =	
  new	
  SchemaTool($this-­‐>getEntityManager());
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  $tool-­‐>dropSchema($metadata);
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  $tool-­‐>createSchema($metadata);
	
  	
  	
  	
  	
  	
  	
  	
  }
	
  	
  	
  	
  }
TableNode
• getRows
• getHash
• getRowsHash
• getRowLines
• getRowAsString
• getNumeratedRows
Scenario:
  Given the   following people exist:
    | name    | email           | phone   |
    | Aslak   | aslak@email.com | 123     |
    | Joe     | joe@email.com   | 234     |
    | Bryan   | bryan@email.org | 456     |




/**
  * @Given /the following people exist:/
  */
public function thePeopleExist(TableNode $table)
{
    $hash = $table->getHash();
    foreach ($hash as $row) {
        // $row['name'], $row['email'], $row['phone']
    }
}
PyStringNode


• Опеределение длинного теста в
  несколько строчек
Scenario:
  Given a blog post named "Random" with:
    """
    Some Title, Eh?
    ===============
    Here is the first paragraph of my blog post.
    Lorem ipsum dolor sit amet, consectetur adipiscing
    elit.
    """




/**
  * @Given /a blog post named "([^"]+)" with:/
  */
public function blogPost($title, PyStringNode $markdown)
{
    $this->createPost($title, $markdown->getRaw());
}
Backgrounds


• Общие шаги для всех сценариев
• Позволяется избавиться от дублирования
  шагов в каждом сценарии
 	
  Background:
	
  	
  	
  	
  Given	
  the	
  site	
  has	
  following	
  users:
	
  	
  	
  	
  |	
  name	
  	
  	
  	
  	
  	
  |
	
  	
  	
  	
  |	
  knplabs	
  	
  	
  |
	
  	
  	
  	
  |	
  fos	
  	
  	
  	
  	
  	
  	
  |
	
  	
  	
  	
  Given	
  the	
  site	
  has	
  following	
  bundles:
	
  	
  	
  	
  |	
  username	
  	
  |	
  name	
  	
  	
  	
  	
  	
  	
  |	
  description	
  |	
  lastCommitAt	
  |	
  score	
  |	
  trend1	
  |
	
  	
  	
  	
  |	
  knplabs	
  	
  	
  |	
  TestBundle	
  |	
  test	
  desc	
  	
  	
  |-­‐1	
  day	
  	
  	
  	
  	
  	
  	
  	
  |	
  10	
  	
  	
  	
  |	
  15	
  	
  	
  	
  	
  |
	
  	
  	
  	
  |	
  fos	
  	
  	
  	
  	
  	
  	
  |	
  UserBundle	
  |	
  user	
  desc	
  	
  	
  |-­‐2	
  days	
  	
  	
  	
  	
  	
  	
  |	
  20	
  	
  	
  	
  |	
  5	
  	
  	
  	
  	
  	
  |
MetaSteps
• Объединяем несколько шагов в один
• Помогает избавиться от дублирования
  шагов
• Тесты запускаются по цепочке
• Возвращаем массив состоящий из шагов,
  которые необходимо выполнить
/**
  * @Given /I entered "([^"]*)" and expect "([^"]*)"/
  */
public function complexStep($number, $result)
{
    return array(
        new StepGiven("I have entered "$number""),
        new StepWhen("I press +"),
        new StepThen("I should see "$result" on the screen")
    );
}
ScenarioOutlines

• Помогает избавиться от дублирования
  сценариев
• Предоставляет удобный интерфейс для
  описания набора тестов и ожидаемых
  результатов
Scenario Outline:
  Given I have entered <number1>
  And I have entered <number2>
  When I add
  Then The result should be <result>

  Examples:
    | number1   |   number2   |   result   |
    | 10        |   12        |   22       |
    | 5         |   3         |   8        |
    | 5         |   5         |   10       |
Tags

• Тэги в сценариях
• Тэги в хуках
• behat --tags "@orm,@database"
• behat --tags "@orm&&@database"
• beaht --tags "-@database"
Запуск сценариев
• behat features/
• behat features/single.feature
• behat features/single.feature:10-20
• behat --name=”Feature name”
• behat --tags @tag1,@tag2
• behat --profile test
Profiles

• Настройки форматтеров
• Настройка контекстов
• Настройки тэгов
• Настройки экстеншенов
• Настройка путей к файлам
# behat.yml
default:
    context:
        class:      YourCustomContext
wip:
    filters:
        tags:       "@wip"
    formatter:
        name:       progress
ci:
    formatter:
        name:       junit
        parameters:
            output_path: /var/tmp/junit
Система экстеншенов
• Mink Extension
• Symfony2 Extension
• Behatch Extension
• Doctrine DataFixtures   Extension
• Gearman Extension
• Write your own
Mink
Зачем?

• Слой абстракции для использования
  различных эмуляторов браузера
• Приемочное тестирование web-
  приложений
Установка

• PHP 5.3
• Composer
• PHAR
• Git
Drivers

• Goutte
• Zombie
• Selenium
• Selenium2
• Sahi
Session

• Через сессию можно получить доступ к
  остальным объектам: страница, статус
  код, куки, заголовки и т. д.
• Несколько сессий запущенных
  одновременно
Selectors

• Named
• CSS
• XPath
• find*
• Traversing
NodeElement
• Можем манипулировать элементом
  найденным по одному из селекторов
• Посмотреть аттрибуты, текст
• Эмулировать события браузера
• Манипулировать элеметами формы,
  ввести текст в инпут, выбрать чекбокс,
  прикрепить файл и т. д.
Mink + Behat

• Mink Extension for Behat
• Mink может быть использован отдельно
  от Behat
• Минимум конфигурации
BDD workflow
• Обсуждение функционала
• Составление User stories
• Подготовка сценариев на Gherkin DSL
• Пишем недостающие Step definitions
• Пишем функционал, юнит тесты, и т. д.
• Проверяем DoD
Спасибо за внимание!

Behat в PHP с использованием Behat и Mink

  • 1.
    BDD в PHP сиспользованием Behat и Mink
  • 2.
    About me • Symfony developer at KnpLabs • twitter: @tyomo4ka • GitHub: tyomo4ka
  • 3.
  • 4.
    Agenda • BDD • GherkinDSL • Behat • Mink • BDD workflow
  • 5.
    Зачем тестировать? • Безопасныйрефакторинг • Отсутствие регрессий • Более качеcтвенная архитектура • Уменьшение числа багов • Степень зрелости разработчика?
  • 6.
  • 7.
    TDD • TDD неочень удачое название • Если мы пишем тесты перед кодом, мы все равно думаем об архитектуре • Design Driven Development?
  • 8.
    TDD и BDD •Также пишем тесты перед кодом • При TDD мы фиксируем в тестах архитектуру приложения или его частей • При BDD мы фиксируем в тестах поведение приложения или его частей • Описательная часть: спецификация или пользовательские сценарии
  • 9.
    Spec BDD • Добавляемописательную часть к тестам • Получаем не тесты, а спецификации объектов • Тестирование системы изнутри • Используем вместо юнит тестов и интеграционых тестов
  • 10.
    Story (Scenario) BDD •Вместо тестов описываем шаги, которые нужно выполнить для достижения определнного результата • Шаги должны легко читаться, в идеале это должны быть простые предложения • Тестирование системы снаружи • Замена функциональным тестам
  • 11.
  • 12.
    Feature: Customer login In order to view protected data As a customer I need to be able to login Background: Given customers are registered: | username | password | blocked | | active@user.com | password | no | | blocked@user.com | password | yes | Scenario: Successful login Given I am on page "Login" When I fill in "Username" with "active@user.com" And I fill in "Password" with "password" And I press "Submit" Then I should be on page "Personal profile" And I should see "Successful login"
  • 13.
    Feature: Название функционала In order to ... Ценность функционала As a ... Выгодополучатель I need ... Краткое описание функционала Background: Given ... Начальное состояние системы Scenario: Название сценария Given ... Начальное состояние And ... Начальное состояние When ... Выполняем шаг And ... Выполняем шаг Then ... Проверяем результат And ... Проверяем результат
  • 14.
  • 15.
    Зачем? • A php framework for testing your business expectations
  • 16.
    Установка • PHP 5.3 •Composer • PHAR • Git
  • 17.
    Инициализация • behat --init •features/ directory • features/bootstrap/ directory • features/bootstrap/*Context.php • behat.yml
  • 18.
    features/*.feature • Gherkin DSL •behat --story-syntax --lang=LANG
  • 19.
    [Feature|Business Need|Ability]: Internaloperations In order to stay secret As a secret organization We need to be able to erase past agents' memory Background: Given there is agent A And there is agent B Scenario: Erasing agent memory Given there is agent J And there is agent K When I erase agent K's memory Then there should be agent J But there should not be agent K [Scenario Outline|Scenario Template]: Erasing other agents' memory Given there is agent <agent1> And there is agent <agent2> When I erase agent <agent2>'s memory Then there should be agent <agent1> But there should not be agent <agent2> [Examples|Scenarios]: | agent1 | agent2 | | D | M |
  • 20.
    Feature:  Listing  developers    As  a  Visitor    I  want  to  browse  through  developers  list    Background:        Given  the  site  has  following  users:        |  name            |        |  knplabs      |        |  fos              |        Given  the  site  has  following  bundles:        |  username    |  name              |  description  |  lastCommitAt  |  score  |  trend1  |        |  knplabs      |  TestBundle  |  test  desc      |-­‐1  day                |  10        |  15          |        |  fos              |  UserBundle  |  user  desc      |-­‐2  days              |  20        |  5            |    Scenario:  Listing  developers        When  I  go  to  "/"        And  I  follow  "Developers"        Then  I  should  see  "2  developers  using  Symfony2"        And  I  should  see  "knplabs"  developer        And  I  should  see  "fos"  developer
  • 21.
    Context • POPO • Описаниешагов • Хуки • Subcontexts • Closures для описания шагов и хуков
  • 22.
           public  function  __construct($kernel)        {                $this-­‐>useContext('symfony_doctrine',  new  SymfonyDoctrineContext());                $this-­‐>useContext('solr',  new  SolrContext());                $this-­‐>useContext('mink',  new  MinkContext());                $this-­‐>useContext('api',  new  ApiContext());        }
  • 23.
    Steps definition • @Given,@When, @Then • Если шаг не выбросил исключение, значит он завершился успешно • Нет своих асершенов, но легко можно использовать асершены из PHPUnit
  • 24.
           /**          *  @Given  /^the  bundles  have  following  keywords:$/          */        public  function  theBundlesHaveFollowingKeywords(TableNode  $table)        {                $entityManager  =  $this-­‐>getEntityManager();                foreach  ($table-­‐>getHash()  as  $row)  {                        if  (isset($this-­‐>bundles[$row['bundle']]))  {                                $bundle  =  $this-­‐>bundles[$row['bundle']];                                $keyword  =  $entityManager -­‐>getRepository('KnpBundleKnpBundlesBundleEntityKeyword') -­‐>findOrCreateOne($row['keyword']);                                $bundle-­‐>addKeyword($keyword);                                $entityManager-­‐>persist($bundle);                        }                }                $entityManager-­‐>flush();        }
  • 25.
    Hooks • BeforeStep/AfterStep • BeforeScenario/AfterScenario •BeforeFeature/AfterFeature • BeforeSuite/AfterSuite • Hooks can be tagged
  • 26.
           /**          *  @BeforeScenario          *          *  @return  null          */        public  function  buildSchema($event)        {                $metadata  =  $this-­‐>getMetadata();                if  (!empty($metadata))  {                        $tool  =  new  SchemaTool($this-­‐>getEntityManager());                        $tool-­‐>dropSchema($metadata);                        $tool-­‐>createSchema($metadata);                }        }
  • 27.
    TableNode • getRows • getHash •getRowsHash • getRowLines • getRowAsString • getNumeratedRows
  • 28.
    Scenario: Giventhe following people exist: | name | email | phone | | Aslak | aslak@email.com | 123 | | Joe | joe@email.com | 234 | | Bryan | bryan@email.org | 456 | /** * @Given /the following people exist:/ */ public function thePeopleExist(TableNode $table) {     $hash = $table->getHash();     foreach ($hash as $row) {         // $row['name'], $row['email'], $row['phone']     } }
  • 29.
    PyStringNode • Опеределение длинноготеста в несколько строчек
  • 30.
    Scenario: Givena blog post named "Random" with: """ Some Title, Eh? =============== Here is the first paragraph of my blog post. Lorem ipsum dolor sit amet, consectetur adipiscing elit. """ /** * @Given /a blog post named "([^"]+)" with:/ */ public function blogPost($title, PyStringNode $markdown) {     $this->createPost($title, $markdown->getRaw()); }
  • 31.
    Backgrounds • Общие шагидля всех сценариев • Позволяется избавиться от дублирования шагов в каждом сценарии
  • 32.
       Background:        Given  the  site  has  following  users:        |  name            |        |  knplabs      |        |  fos              |        Given  the  site  has  following  bundles:        |  username    |  name              |  description  |  lastCommitAt  |  score  |  trend1  |        |  knplabs      |  TestBundle  |  test  desc      |-­‐1  day                |  10        |  15          |        |  fos              |  UserBundle  |  user  desc      |-­‐2  days              |  20        |  5            |
  • 33.
    MetaSteps • Объединяем несколькошагов в один • Помогает избавиться от дублирования шагов • Тесты запускаются по цепочке • Возвращаем массив состоящий из шагов, которые необходимо выполнить
  • 34.
    /** *@Given /I entered "([^"]*)" and expect "([^"]*)"/ */ public function complexStep($number, $result) {     return array(         new StepGiven("I have entered "$number""),         new StepWhen("I press +"),         new StepThen("I should see "$result" on the screen")     ); }
  • 35.
    ScenarioOutlines • Помогает избавитьсяот дублирования сценариев • Предоставляет удобный интерфейс для описания набора тестов и ожидаемых результатов
  • 36.
    Scenario Outline: Given I have entered <number1> And I have entered <number2> When I add Then The result should be <result> Examples: | number1 | number2 | result | | 10 | 12 | 22 | | 5 | 3 | 8 | | 5 | 5 | 10 |
  • 37.
    Tags • Тэги всценариях • Тэги в хуках • behat --tags "@orm,@database" • behat --tags "@orm&&@database" • beaht --tags "-@database"
  • 38.
    Запуск сценариев • behatfeatures/ • behat features/single.feature • behat features/single.feature:10-20 • behat --name=”Feature name” • behat --tags @tag1,@tag2 • behat --profile test
  • 40.
    Profiles • Настройки форматтеров •Настройка контекстов • Настройки тэгов • Настройки экстеншенов • Настройка путей к файлам
  • 41.
    # behat.yml default:     context:         class: YourCustomContext wip:     filters:         tags: "@wip"     formatter:         name: progress ci:     formatter:         name: junit         parameters:             output_path: /var/tmp/junit
  • 42.
    Система экстеншенов • MinkExtension • Symfony2 Extension • Behatch Extension • Doctrine DataFixtures Extension • Gearman Extension • Write your own
  • 43.
  • 44.
    Зачем? • Слой абстракциидля использования различных эмуляторов браузера • Приемочное тестирование web- приложений
  • 45.
    Установка • PHP 5.3 •Composer • PHAR • Git
  • 46.
    Drivers • Goutte • Zombie •Selenium • Selenium2 • Sahi
  • 47.
    Session • Через сессиюможно получить доступ к остальным объектам: страница, статус код, куки, заголовки и т. д. • Несколько сессий запущенных одновременно
  • 48.
    Selectors • Named • CSS •XPath • find* • Traversing
  • 49.
    NodeElement • Можем манипулироватьэлементом найденным по одному из селекторов • Посмотреть аттрибуты, текст • Эмулировать события браузера • Манипулировать элеметами формы, ввести текст в инпут, выбрать чекбокс, прикрепить файл и т. д.
  • 50.
    Mink + Behat •Mink Extension for Behat • Mink может быть использован отдельно от Behat • Минимум конфигурации
  • 51.
  • 52.
    • Обсуждение функционала •Составление User stories • Подготовка сценариев на Gherkin DSL • Пишем недостающие Step definitions • Пишем функционал, юнит тесты, и т. д. • Проверяем DoD
  • 53.