Behavior-Driven Development
с помощью Cucumber

Андрей Михайлов (lolmaus)

Frotnend-разработчик в Kaliber5

 

https://lolma.us

https://github.com/lolmaus

https://kaliber5.de

или как не подавиться огурцом

Пирамида потребностей

Пирамида тестов

Acceptance

Integration

Unit

Пирамида тестов

Acceptance

Integration

Unit

$$$

$

Пирамида тестов

Acceptance

Integration

Unit

$$$

$

Правильная пирамида тестов

Unit

Integration

Acceptance

Acceptance-тесты

  • Дают гарантии
  • Интегрируются в цикл BDD
  • Имеют ценность для бизнеса

Behavior-Driven Development

  • Не просто усложненный вариант
    Test-Driven Development
     
  • Проходит через весь жизенный цикл продукта от планирования до деплоя
     
  • Включает всех членов команды
     
  • Основана на user stories (сценариях)

User stories для регистрации/логина

  • Посещение разделов:
    • Анонимный пользователь посещает публичный раздел приложения, переходит с нее на страницу логина по ссылке.
    • Анонимный пользователь посещает непубличный раздел приложения, и его перенаправляет на страницу логина.
    • Залогиненный пользователь посещает непубличный раздел приложения.
    • Залогиненный пользователь посещает страницу логина, и его перенаправляет в непубличный раздел приложения.
    • Залогиненный пользователь посещает страницу регистрации, и его перенаправляет в непубличный раздел приложения.

 

  • На странице логина анонимный пользователь:
    • вводит правильные логин и пароль, авторизуется и попадает в непубличный раздел приложения.
    • вводит несуществующую комбинацию логин-пароль и видит соответствующую ошибку.
    • пытается залогиниться с пустым логином, кнопка входа неактивна.
    • пытается залогиниться с пустым паролем, кнопка входа неактивна.
  • На странице регистрации анонимный пользователь:
    • успешно заполняет форму, авторизуется и попадает в непубличный раздел приложения.
    • вводит недопустимый логин и видит ошибку (несколько вариантов), кнопка регистрации недоступна.
    • вводит недопустимый пароль и видит ошибку (несколько вариантов), кнопка регистрации недоступна.
    • вводит несоответствующую пару паролей и видит ошибку, кнопка регистрации недоступна.
    • вводит всё правильно, пытается зарегистрироваться, но сервер отвергает пароль как уже занятый.
    • переходит на страницу восстановления пароля.
       
  • На странице восстановления пароля анонимный пользователь:
    • вводит корректный имэйл, отправляет запрос и видит сообщение с просьбой проверить почту.
    • вводит некорректный имэйл, кнопка отправки неактивна (несколько вариантов).
    • вводит корректный имэйл, отправляет запрос, но сервер отвечает отказом, пользовател видит сообщение, что имэйл не используется.

Преимущества
сценариев и BDD

  • Вскрывается истинный объем фич
  • Более реалистичная оценка сроков
  • Сценарии выполняют роль ТЗ
  • Написаны на человеческом языке
  • Служат источником правды для разрешения разногласий
  • Разработчикам легко контролировать объем выполнения фич
  • Сценарии фокусируются на нуждах пользователей
  • Интегрируются с автоматическим тестированием
  • Появился 10 лет назад как библиотека поверх RSpec на языке Ruby.
  • Развился в полноценную методологию.
  • Портирован на многие языки. Есть официальные порты от команды Cucumber и неофициальные аналоги.
  • На JS есть официальный CucumberJS и неофициальный Yadda.
Feature: Sign-up and login
  Covers various cases related to authentication

Scenario: Anonymous user should be able to visit public area and proceed to login
  Given I am not authenticated
  When I visit /
  Then I should be at /
  And I should see a login link
  When I click the login link
  Then I should be at /login

Scenario: Anonymous user should be redirected to login when vising private area
  Given I am not authenticated
  When I visit /cabinet
  Then I should be at /login

Scenario: Authenticated user should be able to visit private area
  Given I am authenticated as a user
  When I visit /cabinet
  Then I should be at /cabinet

sign-up-and-login.feature

синтаксис Gherkin

import {expect} from 'chai';
import waitForSettledState from '../helpers/settled';

export default {
    
    "When I click the Save Button"() {
        const button = document.querySelector('button.save');
        button.click();
        return waitForSettledState();
    }

}

steps.js

имплементация шагов

    
    "Then there should be a success message"() {
        const messages = document.querySelectorAll('.flash-message');
        expect(messages).to.have.length(1);
    }

}
    
    "Then the success message should have text (.+)"(text) {
        const message = document.querySelector('.flash-message');
        expect(message).to.have.trimmed.text(text);
    }

}
    
    "When I click the $index menu item"(indexZero) {
        const menuItems= document.querySelectorAll('.menu-item');
        menuItems[indexZero].click();
        return waitForSettledState();
    }
}

// Возвращаем promise для асинхронности

// Выполняем действия

// Делаем проверки

// Принимаем параметры

// Используем макросы

@SetupApplication
Feature: Sign-up and login: authenticated user
  Covers various cases related to auth for a regular user

Background:
  Given there are records of type User:
    -------------------------
    | ID    | Name  | Role  |
    | alice | Alice | admin |
    | bob   | Bob   | user  |
    -------------------------
  Given I am authenticated as bob

Scenario: Regular user visits [Target URL]
  When I visit [Target URL]
  Then I should be at [Resulting URL]
  And page title should have text [Page Title]

  Where:
    -------------------------------------------------------------------
    | Target URL | Resulting URL | Page Title                         |
    | /cabinet   | /cabinet      | Cabinet                            |
    | /users     | /restricted   | You don't have access to this page |
    -------------------------------------------------------------------

@Skip
Scenario: Admin user visits [Target URL]

sign-up-and-login.feature

синтаксис Gherkin

Преимущества Cucumber

  • Separation of concerns
    Тесты максимально легко читать
    Код не превращается в балласт
     
  • Включение нетехнических сотрудников
    в цикл разработки
     
  • Высокое переиспользование шагов
    Максимальный DRY
    Мгновенное размножение тестов
     
  • Самодисциплина в BDD
     
  • Работодатель счастлив
    Программист счастлив

Проблемы Cucumber

  • Библиотека имплементаций шагов растет бесконтрольно.
    Ориентироваться в ней крайне трудно.
     
  • Каждый шаг — это черный ящик.
     
  • Шаги имеют скрытые зависимости друг от друга.

Начинается всё невинно.
Проверяем посты в блоге:

Given there are 2 Posts in the database
When I visit the blog
Then I should see a list of posts
And there should be two posts in the list

Добавляем проверку заголовков

Given there are 2 Posts in the database
When I visit the blog
Then I should see a list of posts
And there should be two posts in the list

And post 1 title should be "111"
And post 2 title should be "222"

Добавляем проверку комментариев

Given there are 2 Posts in the database
When I visit the blog
Then I should see a list of posts
And there should be two posts in the list

And post 1 title should be "111"
And post 2 title should be "222"

And post 1 should have 2 comments
And post 2 should have 0 comments
And comment 1 of post 2 should have 2 replies

Добавляем Reaction Emoji

Given there are 2 Posts in the database
And comment 4 has reaction emoji "thumbs-up"

When I visit the blog
Then I should see a list of posts
And there should be two posts in the list

And post 1 title should be "111"
And post 2 title should be "222"

And post 1 should have 2 comments
And post 2 should have 0 comments
And comment 1 of post 2 should have 2 replies

And reply 1 of comment 2 to post 1 should have reaction emoji "thumbs-up"

Добавляем закрепленный пост

Given there is a pinned post in the database
And there are 2 Posts in the database
And comment 4 has reaction emoji "thumbs-up"

When I visit the blog
Then I should see a list of posts
And there should be three posts in the list

And post 1 title should be "111"
And post 2 title should be "222"

And post 1 should have 2 comments
And post 2 should have 0 comments
And comment 1 of post 2 should have 2 replies

And reply 1 of comment 2 to post 1 should have reaction emoji "thumbs-up"

Выносим "правду" в feature-файлы.
Сидирование базы данных:

Given there are records of type User in the database:
  -----------------------------
  | id      | name    | admin |
  | "alice" | "Alice" | true  |
  | "bob"   | "Alice" | true  |
  -----------------------------

Given there are records of type Post in the database:
  -----------------------------------------
  | id | title | body  | pinned  | author |
  | 1  | "Hi!" | "Pin" | true    | @alice |
  | 2  | "Aaa" | "A"   |         | @bob   |
  | 3  | "Bbb" | "B"   |         | @bob   |
  -----------------------------------------
  
And there are records of type Comment in the database:
  --------------------------------------------
  | id  | body      | author | post | parent |
  | "1" | "Ololo"   | @alice | @2   |        |
  | "2" | "Trololo" | @bob   |      | @1     |
  --------------------------------------------

Выносим "правду" в feature-файлы.
Шаг посещения страницы:

# Before:
When I visit the blog
Then I should be at the blog

# After:
When I visit /blog
Then I should be at /blog

Выносим "правду" в feature-файлы.
Взаимодействие с DOM:

<ul data-test-main-menu>
  <li data-test-item="home">...</li>
  <li data-test-item="products">...</li>
  <li data-test-item="pricing">...</li>
</ul>

Паттерн "Test Selectors":

Then I should see a [data-test-main-menu]

And there should be 3 [data-test-main-menu] [data-test-menu-item]

When I click [data-test-main-menu] [data-test-menu-item]:nth-child(1)

Применяем в шагах:

Используем labels

Main-Menu                          [data-test-main-menu]
1st Main-Menu                      [data-test-main-menu]:eq(0)
the first Main-Menu                [data-test-main-menu]:eq(0)
first Item in the Main-Menu
[data-test-main-menu] [data-test-item]:eq(0)
Item(Pricing) in the 2nd Main-Menu
[data-test-main-menu]:eq(1) [data-test-item="Pricing"]

Выносим "правду" в feature-файлы.
Взаимодействие с DOM:


labelMap.set('Modal-Dialog', '.modal-dialog');
labelMap.set('Primary-Button', '.btn-primary');

Алиасы для лэйблов:

Автоматическое преобразование алиасов:

the Primary-Button in the Comment-Edit-Form of the Modal-Dialog
.modal-dialog [data-test-comment-edit-form] .primary-button

Пользователю доступно не так много типов действий:

 

When I click the Save-Button in the Post-Edit-Form
When I type "Hello!" into the Title-Field of the Post-Edit-Form 
When I select the 2nd item in the dropdown Sex of the User-Form
When I select the item "Prefer not to tell" in the dropdown Sex of the User-Form
When I move the mouse pointer into the Thumbnail of the 2nd Post
...
Then I should see a Post
Then I should NOT see Posts
Then I should see 2 Posts
Then the Title of the 1st Post should be "Hello!"
Then the first Post should have HTML class "pinned"
Then the first Post should HTML attribute "role" with value "article"
...

Объединение атомарных шагов в один составной

 

When I click on the Trigger of the Tags-Dropdown
Then I should see a Dropdown of the Tags-Dropdown
And I should see a Search field in the Dropdown of the Tags-Dropdown
And I should NOT see a Results-List in the Dropdown of the Tags-Dropdown

When I type "bug" into the Search field of the Dropdown of the Tags-Dropdown
Then I should see a Results-List in the Dropdown of the Tags-Dropdown
And I should see 1 Item in the Results-List in the Dropdown of the Tags-Dropdown
And the Item in the Results-List in the Dropdown of the Tags-Dropdown should have text "bug"

When I click the Item in the Results-List in the Dropdown of the Tags-Dropdown
Then I should NOT see a Dropdown of the Tags-Dropdown
And the Trigger of the Tags-Dropdown should have text "bug"
When I select "bug" in the Tags-Dropdown using look-ahead search

Проблемы Cucumber

  • Трудности с композицией шагов
  • Трудности с отладкой
// CucumberJS
Given('a variable set to {int}', function(number) {
  this.setTo(number)
})

When('I increment the variable by {int}', function(number) {
  this.incrementBy(number)
})

Then('the variable should contain {int}', function(number) {
  expect(this.variable).to.eql(number)
})
// Yadda
export default library
  .given('a variable set to $int', function(number) {
    this.setTo(number)
  })

  .when('I increment the variable by $int', function(number) {
    this.incrementBy(number)
  })

  .then('the variable should contain $int', function(number) {
    expect(this.variable).to.eql(number)
  })
// My opinionated approach

export default {

  'Given a variable set to {int}' (number) {
    this.setTo(number)
  },

  'When I increment the variable by {int}', function(number) {
    this.incrementBy(number)
  },

  'Then the variable should contain {int}', function(number) {
    expect(this.variable).to.eql(number)
  }

}

Text

Всем спасибо! ^_^

Made with Slides.com