Slide 1

Slide 1 text

BDDв php

Slide 2

Slide 2 text

Я кто такой everzet senior from-birth web developer в @

Slide 3

Slide 3 text

Я кто такой senior from-birth web developer в International speaker Разработчик Behat, Mink Разработчик jade.php Разработчик capifony Core-contributor Symfony2 framework Разработчик плагинов symfony и Symfony2 [email protected] http://github.com/everzet http://card.everzet.com everzet @

Slide 4

Slide 4 text

BDD, Symfony2 эксперты Активные контрибуторы в open-source проекты Консультанты, аудиторы, тренеры http://knplabs.com

Slide 5

Slide 5 text

История тестирования

Slide 6

Slide 6 text

UnitTest История Автоматизация тестов

Slide 7

Slide 7 text

UnitTest TDD Тесты вперед История Автоматизация тестов

Slide 8

Slide 8 text

UnitTest TDD Dan North BDD История Тесты вперед Автоматизация тестов

Slide 9

Slide 9 text

BDD ⎯ эволюция TDD

Slide 10

Slide 10 text

- Эволюция-хренолюция... Что не так с TDD?

Slide 11

Slide 11 text

Test-Driven Development

Slide 12

Slide 12 text

Test-Driven Development Мы на самом деле говорим о тестах??? Но каким образом тестировать то, чего еще нет?

Slide 13

Slide 13 text

Test-Driven Development дизайне На самом деле, мы говорим о

Slide 14

Slide 14 text

Test-Driven Development Behavior © 2003, Dan North

Slide 15

Slide 15 text

BDDбыл создан как набор конвенций поверх TDD

Slide 16

Slide 16 text

Тест-кейсы должы составлять предложения BDDбыл создан как набор конвенций поверх TDD testFindsCustomerById() testFailsForDuplicateCustomers()

Slide 17

Slide 17 text

testFindsCustomerById() testFailsForDuplicateCustomers() Тест-кейсы должны начинаться со слова “should” shouldFindCustomerById() shouldFailForDuplicateCustomers() BDDбыл создан как набор конвенций поверх TDD Тест-кейсы должы составлять предложения

Slide 18

Slide 18 text

shouldFindCustomerById() shouldFailForDuplicateCustomers() Класс тест-кейсов должен представлять из себя существительное для кейсов class CustomerTableTest extends \PHPUnitTestCase { /** * @Test */ shouldFindCustomerById() ... } BDDбыл создан как набор конвенций поверх TDD Тест-кейсы должны начинаться со слова “should” Тест-кейсы должы составлять предложения testFindsCustomerById() testFailsForDuplicateCustomers()

Slide 19

Slide 19 text

АССЕРШЕНЫ тоже TEST-ориентированы

Slide 20

Slide 20 text

assertEquals($expected, $actual) assertGreaterThan($expected, $actual) assertInstanceOf($class, $actual) АССЕРШЕНЫ тоже TEST-ориентированы ТЕСТируем

Slide 21

Slide 21 text

assertEquals($expected, $actual) assertGreaterThan($expected, $actual) assertInstanceOf($class, $actual) $actual should be Equals to $expected $actual should be GreaterThan $expected $actual should be InstanceOf $class Описываем АССЕРШЕНЫ тоже TEST-ориентированы ТЕСТируем

Slide 22

Slide 22 text

Specификационные BDD Фрэймворки

Slide 23

Slide 23 text

*Spec RSpec by Dave Astels

Slide 24

Slide 24 text

*Spec RSpec by Dave Astels JSpec by TJ Holowaychuk

Slide 25

Slide 25 text

*Spec RSpec by Dave Astels JSpec by TJ Holowaychuk Fabulous by Alex Rudakov

Slide 26

Slide 26 text

RSpec # bowling_spec.rb require 'bowling' describe Bowling, "#score" do it "returns 0 for all gutter game" do bowling = Bowling.new 20.times { bowling.hit(0) } bowling.score.should == 0 end end

Slide 27

Slide 27 text

RSpec # bowling_spec.rb require 'bowling' describe Bowling, "#score" do it "returns 0 for all gutter game" do bowling = Bowling.new 20.times { bowling.hit(0) } bowling.score.should == 0 end end Пишем СПЕЦИФИКАЦИЮ, а не UnitTEST

Slide 28

Slide 28 text

UnitTest TDD Spec BDD Сначала дизайн Dan North BDD История Тесты вперед Автоматизация тестов

Slide 29

Slide 29 text

BDD СЦЕНАРНЫЙ photo by dsearls

Slide 30

Slide 30 text

photo by dsearls photo by Horia Varlan СЛОВАРЬ

Slide 31

Slide 31 text

photo by dsearls photo by Horia Varlan для тестеров СЛОВАРЬ

Slide 32

Slide 32 text

photo by dsearls photo by Horia Varlan для аналитиков СЛОВАРЬ для тестеров

Slide 33

Slide 33 text

photo by dsearls photo by Horia Varlan для девелоперов СЛОВАРЬ для аналитиков для тестеров

Slide 34

Slide 34 text

photo by dsearls photo by Horia Varlan для заказчиков СЛОВАРЬ для девелоперов для аналитиков для тестеров

Slide 35

Slide 35 text

photo by dsearls photo by Horia Varlan 1 СЛОВАРЬ для заказчиков для девелоперов для аналитиков для тестеров

Slide 36

Slide 36 text

photo by dsearls photo by Horia Varlan тестеры аналитики девелоперы заказчики ИСКОРЕНИТ множество проблем ДИЗАЙНА и КОММУНИКАЦИЙ 1 СЛОВАРЬ

Slide 37

Slide 37 text

КОММУНИКАЦИИ photo by joshfassbind.com

Slide 38

Slide 38 text

In order to [A] As a [B] I need [C] Наратив:

Slide 39

Slide 39 text

Чтобы [A] В качестве [B] Мне нужно [C] Наратив:

Slide 40

Slide 40 text

A ⎯ добавочное знач. (профит) функционала B ⎯ профитирующая персона (роль) C ⎯ функционал Чтобы [A] В качестве [B] Мне нужно [C] Наратив:

Slide 41

Slide 41 text

⎯ Сила данной конструкции в том, что она требует определения профита от функционала еще до его реализации © Dan North A ⎯ добавочное знач. (профит) функционала B ⎯ профитирующая персона (роль) C ⎯ функционал Чтобы [A] В качестве [B] Мне нужно [C] Наратив:

Slide 42

Slide 42 text

Поведение story ⎯ это ее приемочный критерий! ⎯ если система удовлетворяет все приемочные критерии, то она работает верно; если не выполняет - неверно.

Slide 43

Slide 43 text

In order to ... As a ... I need ... Story:

Slide 44

Slide 44 text

Given some initial context (the givens), When an event occurs, Then ensure some outcomes. In order to ... As a ... I need ... Story:

Slide 45

Slide 45 text

Given some initial context (the givens), When an event occurs, Then ensure some outcomes. Given some initial context (the givens), When an event occurs, Then ensure some outcomes. Story: In order to ... As a ... I need ...

Slide 46

Slide 46 text

Scenario 1: Scenario 2: Story: Given some initial context (the givens), When an event occurs, Then ensure some outcomes. Given some initial context (the givens), When an event occurs, Then ensure some outcomes. In order to ... As a ... I need ...

Slide 47

Slide 47 text

UnitTest TDD Spec BDD Scenario BDD Сначала анализ Dan North BDD История Сначала дизайн Тесты вперед Автоматизация тестов

Slide 48

Slide 48 text

Сначала анализ Сначала дизайн UnitTest TDD Spec BDD Scenario BDD Dan North BDD История Тесты вперед Автоматизация тестов +

Slide 49

Slide 49 text

GHERKINDSL photo by isobel.gordon

Slide 50

Slide 50 text

Given some initial context (the givens), When an event occurs, Then ensure some outcomes. In order to ... As a ... I need ... Given some initial context (the givens), When an event occurs, Then ensure some outcomes. Scenario 1: Scenario 2: Story:

Slide 51

Slide 51 text

Given some initial context (the givens), When an event occurs, Then ensure some outcomes. In order to ... As a ... I need ... Given some initial context (the givens), When an event occurs, Then ensure some outcomes. Scenario: 1st scenario title Scenario: 2nd scenario title Feature: Feature description

Slide 52

Slide 52 text

1. feature 2. scenario 3. step ... ... 2. scenario 3. step ... ... Given some initial context (the givens) When an event occurs Then ensure some outcomes In order to ... As a ... I need ... Given some initial context (the givens) When an event occurs Then ensure some outcomes Scenario: 1st scenario title Scenario: 2nd scenario title Feature: Feature description feature tree

Slide 53

Slide 53 text

Given some initial context (the givens) When an event occurs Then ensure some outcomes In order to ... As a ... I need ... Given some initial context (the givens) When an event occurs Then ensure some outcomes Scenario: 1st scenario title Scenario: 2nd scenario title Feature: Feature description

Slide 54

Slide 54 text

Etant donné some initial context (the givens) Lorsque an event occurs Alors ensure some outcomes In order to ... As a ... I need ... Etant donné some initial context (the givens) Lorsque an event occurs Alors ensure some outcomes Scénario: 1st scenario title Scénario: 2nd scenario title Fonctionnalité: Feature description # language: fr

Slide 55

Slide 55 text

ͳΒ͹ some initial context (the givens) ͔͠͠ an event occurs લఏ ensure some outcomes In order to ... As a ... I need ... ͳΒ͹ some initial context (the givens) ͔͠͠ an event occurs લఏ ensure some outcomes γφϦΦ: 1st scenario title γφϦΦ: 2nd scenario title ϑΟʔνϟ: Feature description # language: ja

Slide 56

Slide 56 text

Допустим some initial context (the givens) Когда an event occurs То ensure some outcomes In order to ... As a ... I need ... Допустим some initial context (the givens) Когда an event occurs То ensure some outcomes Сценарий: 1st scenario title Сценарий: 2nd scenario title Функционал: Feature description # language: ru

Slide 57

Slide 57 text

Let go and haul some initial context (the givens) Blimey! an event occurs Aye ensure some outcomes In order to ... As a ... I need ... Let go and haul some initial context (the givens) Blimey! an event occurs Aye ensure some outcomes Heave to: 1st scenario title Heave to: 2nd scenario title Ahoy matey!: Feature description # language: en-pirate

Slide 58

Slide 58 text

Let go and haul some initial context (the givens) Blimey! an event occurs Aye ensure some outcomes Let go and haul some initial context (the givens) Blimey! an event occurs Aye ensure some outcomes Heave to: Heave to: Ahoy matey!: # language: en-pirate

Slide 59

Slide 59 text

Приемочные критерии должны быть исполняемы!

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

Установка 1. Добавляем pear-channel: $ pear channel-discover pear.behat.org 2. Ставим: $ pear install behat/behat 3. Инициализируем: $ cd path/to/project && behat --init

Slide 62

Slide 62 text

Установка 1. Добавляем pear-channel: $ pear channel-discover pear.behat.org 2. Ставим: $ pear install behat/behat 3. Инициализируем: $ cd path/to/project && behat --init +d features - place your *.feature files here +d features/steps - place step definition files here +f features/steps/steps.php - place some step definitions in this file +d features/support - place support scripts and static files here +f features/support/bootstrap.php - place bootstrap scripts in this file +f features/support/env.php - place environment initialization scripts in this file

Slide 63

Slide 63 text

# language: ru Функционал: Утилита ls Чтобы узнать содержимое директории Как пользователь UNIX Я должен иметь утилиту листинга директорий

Slide 64

Slide 64 text

Сценарий: 2 файла в директории Чтобы узнать содержимое директории Как пользователь UNIX Я должен иметь утилиту листинга директорий # language: ru Функционал: Утилита ls

Slide 65

Slide 65 text

Допустим я нахожусь в директории “test1” Если я исполню “ls” То я должен увидеть: Сценарий: 2 файла в директории Чтобы узнать содержимое директории Как пользователь UNIX Я должен иметь утилиту листинга директорий # language: ru Функционал: Утилита ls """ file_one.txt file_foo.txt """

Slide 66

Slide 66 text

1. feature 2. scenario Допустим я нахожусь в директории “test1” Если я исполню “ls” То я должен увидеть: Сценарий: 2 файла в директории Чтобы узнать содержимое директории Как пользователь UNIX Я должен иметь утилиту листинга директорий # language: ru Функционал: Утилита ls """ file_one.txt file_foo.txt """

Slide 67

Slide 67 text

Допустим я нахожусь в директории “test1” Если я исполню “ls” То я должен увидеть: Сценарий: 2 файла в директории Чтобы узнать содержимое директории Как пользователь UNIX Я должен иметь утилиту листинга директорий # language: ru Функционал: Утилита ls """ file_one.txt file_foo.txt """

Slide 68

Slide 68 text

Допустим я нахожусь в директории “test1” ОПРЕДЕЛЕНИЯШАГОВ

Slide 69

Slide 69 text

Slide 70

Slide 70 text

Slide 71

Slide 71 text

Допустим('/^я нахожусь в директории “(.*)”$/', function() { throw new \Behat\Behat\Exception\Pending(); } ); ОПРЕДЕЛЕНИЯШАГОВ Допустим я нахожусь в директории “test1”

Slide 72

Slide 72 text

Допустим('/^я нахожусь в директории “(.*)”$/', function() { throw new \Behat\Behat\Exception\Pending(); } ); ??? ОПРЕДЕЛЕНИЯШАГОВ Допустим я нахожусь в директории “test1”

Slide 73

Slide 73 text

ТИПЫРЕЗУЛЬТАТОВШАГОВ

Slide 74

Slide 74 text

ТИПЫРЕЗУЛЬТАТОВШАГОВ 1. Pending шаг ⎯ который throw new \Behat\Behat\Exception\Pending();

Slide 75

Slide 75 text

1. Pending шаг ⎯ который throw new \Behat\Behat\Exception\Pending(); 2. Undefined шаг ⎯ у которого нет (не найдено) определений ТИПЫРЕЗУЛЬТАТОВШАГОВ

Slide 76

Slide 76 text

1. Pending шаг ⎯ который throw new \Behat\Behat\Exception\Pending(); 2. Undefined шаг ⎯ у которого нет (не найдено) определений 3. Ambiguous шаг ⎯ который подпадает под несколько определений ТИПЫРЕЗУЛЬТАТОВШАГОВ

Slide 77

Slide 77 text

1. Pending шаг ⎯ который throw new \Behat\Behat\Exception\Pending(); 2. Undefined шаг ⎯ у которого нет (не найдено) определений 3. Ambiguous шаг ⎯ который подпадает под несколько определений 4. Failed шаг ⎯ который throw \Exception(); ТИПЫРЕЗУЛЬТАТОВШАГОВ

Slide 78

Slide 78 text

1. Pending шаг ⎯ который throw new \Behat\Behat\Exception\Pending(); 2. Undefined шаг ⎯ у которого нет (не найдено) определений 3. Ambiguous шаг ⎯ который подпадает под несколько определений 4. Failed шаг ⎯ который throw \Exception(); 5. Skipped шаг ⎯ который идет следом за pending/undefined/failed в сценарии ТИПЫРЕЗУЛЬТАТОВШАГОВ

Slide 79

Slide 79 text

1. Pending шаг ⎯ который throw new \Behat\Behat\Exception\Pending(); 2. Undefined шаг ⎯ у которого нет (не найдено) определений 3. Ambiguous шаг ⎯ который подпадает под несколько определений 4. Failed шаг ⎯ который throw \Exception(); 5. Skipped шаг ⎯ который идет следом за pending/undefined/failed в сценарии 6. Passed шаг ⎯ который не кидает эксепшенов ТИПЫРЕЗУЛЬТАТОВШАГОВ

Slide 80

Slide 80 text

ОПРЕДЕЛЕНИЯШАГОВ Допустим('/^я нахожусь в директории “(.*)”$/', function() { throw new \Behat\Behat\Exception\Pending(); } ); Допустим я нахожусь в директории “test1”

Slide 81

Slide 81 text

ОПРЕДЕЛЕНИЯШАГОВ Если я исполню “ls” Допустим('/^я нахожусь в директории “(.*)”$/', function() { throw new \Behat\Behat\Exception\Pending(); } ); Допустим я нахожусь в директории “test1”

Slide 82

Slide 82 text

Если('/^я исполню “(.*)”$/', function($dollars) { throw new \Behat\Behat\Exception\Pending(); } ); ОПРЕДЕЛЕНИЯШАГОВ Если я исполню “ls” Допустим('/^я нахожусь в директории “(.*)”$/', function() { throw new \Behat\Behat\Exception\Pending(); } ); Допустим я нахожусь в директории “test1”

Slide 83

Slide 83 text

Если('/^я исполню “(.*)”$/', function($command) { // $command === “ls” } ); ОПРЕДЕЛЕНИЯШАГОВ Если я исполню “ls” Допустим('/^я нахожусь в директории “(.*)”$/', function($dir) { // $dir === “test1” } ); Допустим я нахожусь в директории “test1”

Slide 84

Slide 84 text

Если('/^я исполню “(.*)”$/', function($command) { exec($command, $output); $output = trim(implode(“\n”, $output)); } ); ОПРЕДЕЛЕНИЯШАГОВ Если я исполню “ls” Допустим('/^я нахожусь в директории “(.*)”$/', function($dir) { chdir('fixtures/' . $dir); } ); Допустим я нахожусь в директории “test1”

Slide 85

Slide 85 text

Если('/^я исполню “(.*)”$/', function($world, $command) { exec($command, $output); $world->output = trim(implode(“\n”, $output)); } ); ОПРЕДЕЛЕНИЯШАГОВ Если я исполню “ls” Допустим('/^я нахожусь в директории “(.*)”$/', function($world, $dir) { chdir('fixtures/' . $dir); } ); Допустим я нахожусь в директории “test1”

Slide 86

Slide 86 text

ПРОВЕРЯЕМРЕЗУЛЬТАТЫ То я должен увидеть:

Slide 87

Slide 87 text

То('/^я должен увидеть:$/', function($world, $string) { if ($world->output !== (string) $string) { throw new \Exception('Неверный вывод'); } } ); ПРОВЕРЯЕМРЕЗУЛЬТАТЫ То я должен увидеть:

Slide 88

Slide 88 text

То('/^я должен увидеть:$/', function($world, $string) { assertEquals((string) $string, $world->output); } ); using PHPUnit То('/^я должен увидеть:$/', function($world, $string) { if ($world->output !== (string) $string) { throw new \Exception('Неверный вывод'); } } ); ПРОВЕРЯЕМРЕЗУЛЬТАТЫ То я должен увидеть: То я должен увидеть: ( )

Slide 89

Slide 89 text

Допустим('/^я нахожусь в директории “(.*)”$/', function($world, $dir) { chdir('fixtures/' . $dir); } ); $steps->Если('/^я исполню “(.*)”$/', function($world, $command) { exec($command, $output); $world->output = trim(implode(“\n”, $output)); } ); $steps->То('/^я должен увидеть:$/', function($world, $string) { assertEquals((string) $string, $world->output); } ); ОПРЕДЕЛЕНИЯШАГОВ

Slide 90

Slide 90 text

Допустим('/^я нахожусь в директории “(.*)”$/', function($world, $dir) { chdir('fixtures/' . $dir); } )-> Если('/^я исполню “(.*)”$/', function($world, $command) { exec($command, $output); $world->output = trim(implode(“\n”, $output)); } )-> То('/^я должен увидеть:$/', function($world, $string) { assertEquals((string) $string, $world->output); } ) ; ОПРЕДЕЛЕНИЯШАГОВ

Slide 91

Slide 91 text

1. Описываем поведение Workflow

Slide 92

Slide 92 text

$ behat features/ 1. Описываем поведение 2. Проверяем поведение ( ) Workflow

Slide 93

Slide 93 text

$ behat features/ 1. Описываем поведение 2. Проверяем поведение ( ) 3. Реализуем поведение Workflow

Slide 94

Slide 94 text

$ behat features/ 1. Описываем поведение 2. Проверяем поведение ( ) 4. Проверяем поведение 3. Реализуем поведение Workflow

Slide 95

Slide 95 text

$ behat features/ 1. Описываем поведение 2. Проверяем поведение ( ) 4. Проверяем поведение 3. Реализуем поведение Workflow

Slide 96

Slide 96 text

3.1. Пишем спеки $ behat features/ 1. Описываем поведение 2. Проверяем поведение ( ) 3.4. Прогоняем спеки 3.3. Пишем код 3.2. Прогоняем спеки 4. Проверяем поведение 3. Реализуем поведение: Workflow

Slide 97

Slide 97 text

3.1. Пишем спеки $ behat features/ 1. Описываем поведение 3.4. Прогоняем спеки 3.3. Пишем код 3.2. Прогоняем спеки 3. Реализуем поведение: 2. Проверяем поведение ( ) 4. Проверяем поведение Workflow

Slide 98

Slide 98 text

Описание web- приложений

Slide 99

Slide 99 text

M!"#

Slide 100

Slide 100 text

Установка 1. Добавляем pear-channel: $ pear channel-discover pear.behat.org 2. Ставим: $ pear install behat/mink-beta

Slide 101

Slide 101 text

registerSession('goutte', new Session(new GoutteDriver($startUrl)) ); $mink->registerSession('javascript',, new Session(new SahiDriver($startUrl, 'firefox')) ); // выполняем действия в стандартном драйвере $mink->getSession('goutte')->getPage()->clickLink('Downloads'); echo $mink->getSession('goutte')->getPage()->getContent(); // выполняем действия в javascript (Sahi) сессии $mink->getSession('javascript')->getPage()->clickLink('Downloads'); echo $mink->getSession('javascript')->getPage()->getContent();

Slide 102

Slide 102 text

Новый проект 1. Создаем каркас проекта: $ cd path/to/project && zf ...

Slide 103

Slide 103 text

Новый проект 1. Создаем каркас проекта: $ cd path/to/project && zf ... Getting Started with Zend Framework By Rob Allen, www.akrabat.com Document Revision 1.7.6 Copyright © 2006, 2010

Slide 104

Slide 104 text

No content

Slide 105

Slide 105 text

No content

Slide 106

Slide 106 text

Новый проект 1. Создаем каркас проекта: $ cd path/to/project && zf ... 2. Инициализируем B$%&': $ behat --init

Slide 107

Slide 107 text

Новый проект 3. Знакомим B$%&' с M!"#: $ vim behat.yml # behat.yml default: environment: parameters: start_url: http://tutorial.zf.dev/ imports: - mink/behat.yml $ vim features/support/bootstrap.php

Slide 108

Slide 108 text

Чтобы иметь представление об исполнителях Как каталогизатор Я должен уметь управлять коллекцией альбомов Сценарий: Добавление альбома Допустим я на странице /index/add Если я ввожу "Pendulum" в поле "Artist" И я ввожу "In Silico" в поле "Title" И нажимаю "Add" То я должен видеть "In Cilico" И я должен видеть "Edit" # language: ru Функционал: Альбомы

Slide 109

Slide 109 text

No content

Slide 110

Slide 110 text

Чтобы иметь представление об исполнителях Как каталогизатор Я должен уметь управлять коллекцией альбомов Сценарий: Добавление альбома Допустим я на странице /index/add Если я ввожу "Pendulum" в поле "Artist" И я ввожу "In Silico" в поле "Title" И нажимаю "Add" То я должен видеть "In Cilico" И я должен видеть "Edit" # language: ru Функционал: Альбомы

Slide 111

Slide 111 text

No content

Slide 112

Slide 112 text

No content

Slide 113

Slide 113 text

Чтобы иметь представление об исполнителях Как каталогизатор Я должен уметь управлять коллекцией альбомов Сценарий: Добавление альбома Допустим в базе нет альбомов И я на странице /index/add Если я ввожу "Pendulum" в поле "Artist" И я ввожу "In Silico" в поле "Title" И нажимаю "Add" То я должен видеть "In Silico" И я должен видеть "Edit" # language: ru Функционал: Альбомы

Slide 114

Slide 114 text

No content

Slide 115

Slide 115 text

Допустим('/^в базе нет альбомов$/', function($world) { $albums = new Application_Model_DbTable_Albums(); $albums->delete(1); } );

Slide 116

Slide 116 text

Чтобы иметь представление об исполнителях Как каталогизатор Я должен уметь управлять коллекцией альбомов Сценарий: Добавление альбома Допустим в базе нет альбомов И я на странице /index/add Если я ввожу "Pendulum" в поле "Artist" И я ввожу "In Silico" в поле "Title" И нажимаю "Add" То я должен видеть "In Silico" И я должен видеть "Edit" # language: ru Функционал: Альбомы

Slide 117

Slide 117 text

Чтобы иметь представление об исполнителях Как каталогизатор Я должен уметь управлять коллекцией альбомов @javascript Сценарий: Добавление альбома Допустим в базе нет альбомов И я на странице /index/add Если я ввожу "Pendulum" в поле "Artist" И я ввожу "In Silico" в поле "Title" И нажимаю "Add" То я должен видеть "In Silico" И я должен видеть "Edit" # language: ru Функционал: Альбомы

Slide 118

Slide 118 text

http://B$%&'.org

Slide 119

Slide 119 text

http://knplabs.com/trainings http://github.com/behat http://groups.google.com/behat

Slide 120

Slide 120 text

Вопросы? http://github.com/behat http://knplabs.com/trainings http://groups.google.com/behat