78
BDD and browser automation with Geb and Spock Данилюк Богдан Jeeconf | may 2014 | @bogdand

2014 Jeeconf - Geb Spock

Embed Size (px)

DESCRIPTION

With application and team growth such questions as keeping documentation up to date, sharing of the knowledge, communication between stakeholder and development team became more and more actual. BDD as methodology is aimed to minimize negative impact of those issues. Spock and Geb frameworks will help us to illustrate BDD implementation on specific example.

Citation preview

Page 1: 2014 Jeeconf - Geb Spock

BDD and browser automation with Geb and Spock

Данилюк Богдан

Jeeconf | may 2014 | @bogdand

Page 2: 2014 Jeeconf - Geb Spock

Главный вопрос жизни вселенной и всего такого

Page 3: 2014 Jeeconf - Geb Spock

Главный вопрос жизни вселенной и всего такого

Как в любой момент быть уверенным в качестве текущей версии вашего

приложения и дать возможность людям понять что же оно делает?

Page 4: 2014 Jeeconf - Geb Spock

Кто я?

● C Groovy / Grails начиная с 2008 года

● Успешно внедрили и используем Geb /

Spock

● - Groovy / Grails курс

● Первый разработчик в TransferWise,

сейчас 100+ человек

Page 5: 2014 Jeeconf - Geb Spock

TransferWise

Page 6: 2014 Jeeconf - Geb Spock

TransferWise

Page 7: 2014 Jeeconf - Geb Spock

С чего все начиналось?

Page 8: 2014 Jeeconf - Geb Spock

≈ 0% покрытие тестами

С чего все начиналось?

Page 9: 2014 Jeeconf - Geb Spock

Smoke testing

Page 10: 2014 Jeeconf - Geb Spock

● Тестировалось <10 страниц

● Время прохождения тестов ≈1 минута

● Обнаруженных ошибок - мало =)

Smoke testing

Page 11: 2014 Jeeconf - Geb Spock

Gebvery groovy browser automation

Page 12: 2014 Jeeconf - Geb Spock

Geb

● Groovy обертка вокруг Selenium 2

● Лицензия ASL2

● Текщая версия: 0.9.2

● Дружит с JUnit и Spock

● Groovy Page object

● http://gebish.org

Page 13: 2014 Jeeconf - Geb Spock

Geb - WebDriver

● Мобильные браузеры:○ iPad○ iPhone○ Android○ Blackberry

● PhantomJS● HtmlUnit

Page 14: 2014 Jeeconf - Geb Spock

Простой Geb test

go "http://www.google.com"

$("input", name: "q").value("JeeConf")

$("button", name: "btnG").click()

waitFor { $("div", id: "search").displayed }

assert $("div", id: "search").text()

.contains("jeeconf.com")

Page 15: 2014 Jeeconf - Geb Spock

Простой Geb test

go "http://www.google.com"

$("input", name: "q").value("JeeConf")

$("button", name: "btnG").click()

waitFor { $("div", id: "search").displayed }

assert $("div", id: "search").text()

.contains("jeeconf.com")

Page 16: 2014 Jeeconf - Geb Spock

Простой Geb test

go "http://www.google.com"

$("input", name: "q").value("JeeConf")

$("button", name: "btnG").click()

waitFor { $("div", id: "search").displayed }

assert $("div", id: "search").text()

.contains("jeeconf.com")

Page 17: 2014 Jeeconf - Geb Spock

Простой Geb test

go "http://www.google.com"

$("input", name: "q").value("JeeConf")

$("button", name: "btnG").click()

waitFor { $("div", id: "search").displayed }

assert $("div", id: "search").text()

.contains("jeeconf.com")

Page 18: 2014 Jeeconf - Geb Spock

Простой Geb test

go "http://www.google.com"

$("input", name: "q").value("JeeConf")

$("button", name: "btnG").click()

waitFor { $("div", id: "search").displayed }

assert $("div", id: "search").text()

.contains("jeeconf.com")

Page 19: 2014 Jeeconf - Geb Spock

Простой Geb test

go "http://www.google.com"

$("input", name: "q").value("JeeConf")

$("button", name: "btnG").click()

waitFor { $("div", id: "search").displayed }

assert $("div", id: "search").text()

.contains("jeeconf.com")

Page 20: 2014 Jeeconf - Geb Spock

Метод $()

$(«css selector», «index or range», «attribute / text matchers»)

Примеры

$("div") // все div элементы

$("div", 0) // первый div элемент

$("div", 0..2) // первых три div элемента

// Третий H2 элемент с текстом “Geb”

$("h2", 2, text: "Geb")

Page 21: 2014 Jeeconf - Geb Spock

CSS3 селекторы

$("div.some-class p:first[title='some']")

$("ul li a")

$("table tr:nth-child(2n+1) td")

$("div#content p:first-child::first-line")

Page 22: 2014 Jeeconf - Geb Spock

Атрибуты

Поиск по атрибутам//<div foo="bar">

$("div", foo: "bar")

Page 23: 2014 Jeeconf - Geb Spock

Атрибуты

Поиск по атрибутам//<div foo="bar">

$("div", foo: "bar")

Поиск по тексту//<div>foo</div>

$("div", text: "foo")

Page 24: 2014 Jeeconf - Geb Spock

Навигация

Поиск по атрибутам//<div foo="bar">

$("div", foo: "bar")

Поиск по тексту//<div>foo</div>

$("div", text: "foo")

Поиск по регулярным выражениям//<div>foo</div>

$("div", text: ~/f.+/)

Page 25: 2014 Jeeconf - Geb Spock

Вспомогательные функции

$("p", text: startsWith("p"))

$("p", class: contains("section"))

$("p", id: endsWith(~/\d/))

Больше примеров в документации

Page 26: 2014 Jeeconf - Geb Spock

Относительный поиск

$("p").previous()

$("p").prevAll()

$("p").next()

$("p").parent()

$("p").siblings()

$("div").children()

$("p").nextAll(".listing")

Page 27: 2014 Jeeconf - Geb Spock

Functional testing

Page 28: 2014 Jeeconf - Geb Spock

Functional testing

● Время прохождения тестов - 20 минут

● Обнаруженных ошибок - уже больше =)

● Грядут редизайны веб страниц

Page 29: 2014 Jeeconf - Geb Spock

GebСтраницы и модули

Page 30: 2014 Jeeconf - Geb Spock

Page Object

class WikipediaPage extends Page {

static url = "https://www.wikipedia.org"

static at = { title == "Wikipedia" }

static content = {

search { $("#searchInput") }

}

}

Page 31: 2014 Jeeconf - Geb Spock

Page Object

class WikipediaPage extends Page {

static url = "https://www.wikipedia.org"

static at = { title == "Wikipedia" }

static content = {

search { $("#searchInput") }

}

}

Page 32: 2014 Jeeconf - Geb Spock

Page Object

class WikipediaPage extends Page {

static url = "https://www.wikipedia.org"

static at = { title == "Wikipedia" }

static content = {

search { $("#searchInput") }

}

}

Page 33: 2014 Jeeconf - Geb Spock

Page Object

class WikipediaPage extends Page {

static url = "https://www.wikipedia.org"

static at = { title == "Wikipedia" }

static content = {

search { $("#searchInput") }

}

}

Page 34: 2014 Jeeconf - Geb Spock

Page Object использование

Browser.drive {

to WikipediaPage

assert at(WikipediaPage)

search = "JeeConf"

}

Page 35: 2014 Jeeconf - Geb Spock

Опциональный контент

class OptionalPage extends Page {

static content = {

errorMsg(required: false) { $("p.errorMsg") }

}

}

Page 36: 2014 Jeeconf - Geb Spock

Динамический контент

class DynamicPage extends Page { static content = { errorMsg(wait: true) { $("p.errorMsg") } }}

Page 37: 2014 Jeeconf - Geb Spock

Наследование Page Object

● Блоки content будут слиты

● Методы унаследуются

Page 38: 2014 Jeeconf - Geb Spock

Повторное использование

● Блоки, повторяющиеся на многих страницах

● Блоки, повторяющиеся на одной странице

Page 39: 2014 Jeeconf - Geb Spock

Объявление модуля

class NavBarModule extends Module {

static content = {

homePageLink(to: HomePage) { $("a#home") }

profileLink(to: ProfilePage) { $("a#profile") }

}

}

Page 40: 2014 Jeeconf - Geb Spock

Использование модулей

class HomePage extends Page {

static content = {

navBarModule { module NavBarModule }

}

void goToProfilePage() {

navBarModule.profileLink.click()

}

}

Page 41: 2014 Jeeconf - Geb Spock

Пример moduleList

class ProfilePage extends Page {

static content = {

paymentListTable {moduleList PaymentRow,

$("table#paymentList tbody tr")}

}

}

class PaymentRow extends Module {

static content = {

amount { $("td.amount") }

status { $("td.status") }

}

}

Page 42: 2014 Jeeconf - Geb Spock

Проблема поддержки

to HomePage

loginButton.click()

username = "user"

password = "password"

loginButton.click()

Page 43: 2014 Jeeconf - Geb Spock

Page Object builder

● Подход предложен Craig Atkinson

● По умолчанию Geb делегирует вызовы методов текущей странице

● Билдер делает эти вызовы явными

Page 44: 2014 Jeeconf - Geb Spock

Page Object builder

HomePage homePage = to HomePage

LoginPage loginPage = homePage.clickLoginButton()

DashboardPage dashboardPage = loginPage.login("user",

"password")

HomePage homePage = to HomePage

DashboardPage dashboardPage = homePage.clickLoginButton().

login("user", "password")

Вызов по цепочке

Page 45: 2014 Jeeconf - Geb Spock

Реализация HomePage

class HomePage extends Page {

static content = {

loginButton(to: LoginPage, wait: true) {

$("#loginButton")

}

}

LoginPage clickLoginButton() {

loginButton.click()

return browser.page

}

}

Page 46: 2014 Jeeconf - Geb Spock

Geb + Spock

Page 47: 2014 Jeeconf - Geb Spock

Архитектура

App

Browser

WebDriverGebgeb-spock(Testing adapter)

Spok

Page 48: 2014 Jeeconf - Geb Spock

Spock тест

class GoogleSpec extends GebReportingSpec {

def "the first link should be wikipedia"() {

when:

to GoogleHomePage

q = "wikipedia"

then:

at GoogleResultsPage

firstResultLink.text() == "Wikipedia"

when:

firstResultLink.click()

then:

waitFor { at WikipediaPage }

}

}

Page 49: 2014 Jeeconf - Geb Spock

Spock data-driven тест

when:

LoginPage loginPage = loginAsUser(username)

then:

assert loginPage.error == expectedErrorMessage

where:

username | expectedErrorMessage

'disabledUser' | 'Sorry, your account is disabled'

'lockedUser' | 'Sorry, your account is locked'

'invalidUser' | 'Sorry, we could not find that account'

Page 50: 2014 Jeeconf - Geb Spock

Проблемы

● Тестировались в основном длительные цепочки страниц

● Вся подготовка данных делалась через веб интерфейс

Page 51: 2014 Jeeconf - Geb Spock

Некрасивый тест

class LoginSpec extends GebReportingSpec {

def "login"() {

given: "a valid user"

go RegistrationPage

register("user", "password")

logout()

when: "the user logins with valid credentials"

go LoginPage

login("user", "password")

then: "the welcome page is displayed"

at DashboardPage

}

Page 52: 2014 Jeeconf - Geb Spock

Некрасивый тест

class LoginSpec extends GebReportingSpec {

def "login"() {

given: "a valid user"

go RegistrationPage

register("user", "password")

logout()

when: "the user logins with valid credentials"

go LoginPage

login("user", "password")

then: "the welcome page is displayed"

at DashboardPage

}

Page 53: 2014 Jeeconf - Geb Spock

Use case тестирование

Page 54: 2014 Jeeconf - Geb Spock

Чего мы хотим?

● Результат тестов не булевое значение

● Экранирование сценариев

● Упор на документирование

● Привлечение нетехнических специалистов для работы с тестами

Page 55: 2014 Jeeconf - Geb Spock

Экранирование сценариев

Чего мы хотим достигнуть?

● Каждый тест готовит данные для себя

● Тест должен знать как можно меньше информации о внутренностях приложения

● Тест не должен ломаться при рефакторингах

Page 56: 2014 Jeeconf - Geb Spock

Первая идея

А давайте обновлять базу?

Проблемы:

● Тесты знают много низкоуровневой информации о приложении

● Тысты очень чуствительны к изменениям в приложении

Page 57: 2014 Jeeconf - Geb Spock

Remote control

Groovy remote control

“is a library for executing closures defined in one Groovy application to be executed in a different (possible remote) Groovy application.”

Page 58: 2014 Jeeconf - Geb Spock

Remote control - сервер

def receiver = new Receiver()

def handler = new RemoteControlHttpHandler(receiver)

def server =

HttpServer.create(new InetSocketAddress(8080), 0)

server.createContext("/groovy-rc", handler)

server.start()

Page 59: 2014 Jeeconf - Geb Spock

Remote control - сервер

def receiver = new Receiver()

def handler = new RemoteControlHttpHandler(receiver)

def server =

HttpServer.create(new InetSocketAddress(8080), 0)

server.createContext("/groovy-rc", handler)

server.start()

Page 60: 2014 Jeeconf - Geb Spock

Remote control - сервер

def receiver = new Receiver()

def handler = new RemoteControlHttpHandler(receiver)

def server =

HttpServer.create(new InetSocketAddress(8080), 0)

server.createContext("/groovy-rc", handler)

server.start()

Page 61: 2014 Jeeconf - Geb Spock

Remote control - клиент

def transport =

new HttpTransport("http://example.org:8080/groovy-rc")

def remote = new RemoteControl(transport)

def id = remote {

def user = new User(name: "Me", password: "pwd")

user.save()

user.id

}

Page 62: 2014 Jeeconf - Geb Spock

Remote control - клиент

def transport =

new HttpTransport("http://example.org:8080/groovy-rc")

def remote = new RemoteControl(transport)

def id = remote {

def user = new User(name: "Me", password: "pwd")

user.save()

user.id

}

Page 63: 2014 Jeeconf - Geb Spock

Remote control - клиент

def transport =

new HttpTransport("http://example.org:8080/groovy-rc")

def remote = new RemoteControl(transport)

def id = remote {

def user = new User(name: "Me", password: "pwd")

user.save()

user.id

}

Page 64: 2014 Jeeconf - Geb Spock

Remote control - клиент

def transport =

new HttpTransport("http://example.org:8080/groovy-rc")

def remote = new RemoteControl(transport)

def id = remote {

def user = new User(name: "Me", password: "pwd")

user.save()

user.id

}

Page 65: 2014 Jeeconf - Geb Spock

Некрасивый тест

class LoginSpec extends GebReportingSpec {

def "login"() {

given: "a valid user"

go RegistrationPage

register("user", "password")

logout()

when: "the user logins with valid credentials"

go LoginPage

login("user", "password")

then: "the welcome page is displayed"

at DashboardPage

}

Page 66: 2014 Jeeconf - Geb Spock

Красивый тест

class LoginSpec extends GebReportingSpec {

def "login"() {

given: "a valid user"

remote {

SpringUtils.getRegisterService()

.register("user", "password")

}

when: "the user logins with valid credentials"

go LoginPage

login("user", "password")

then: "the welcome page is displayed"

at DashboardPage

Page 67: 2014 Jeeconf - Geb Spock

Отчеты

Page 68: 2014 Jeeconf - Geb Spock

Стандартный Grails отчет

Page 69: 2014 Jeeconf - Geb Spock

Стандартный Grails отчет

Page 70: 2014 Jeeconf - Geb Spock

Spock отчеты

Требования

● Логическая групировка тестов

● Возможность запуска группы тестов

● Человекочитаемая документация

Page 71: 2014 Jeeconf - Geb Spock

Пример отчета

Page 72: 2014 Jeeconf - Geb Spock
Page 73: 2014 Jeeconf - Geb Spock

Spock отчеты

Решение

● Расширили Athaydes Spock Reports

○ Добавили группировку

○ Возможность запуска группы спецификаций

Page 74: 2014 Jeeconf - Geb Spock

@Group('Invite program')

@SpecName('Invitee')

class InviteesSpec extends GebBaseSpec {

def "Invitee should get a free payment"() {

when: "user register with invite link"

then: "user has one free payment"

}

}

Page 75: 2014 Jeeconf - Geb Spock

Что дальше?

● Тестирование верстки

● Ускорение тестов

● Переход на Gherkin (Cucumber) для избежания дублирования коментариев и кода

Page 76: 2014 Jeeconf - Geb Spock

class InviteesSpec extends GebBaseSpec {

def "Invitee should get a free payment"() {

when: "registered with invite link user"

registeredWithInviteLinkUser()

then: "user has one free payment"

to AccountPage

freePayments.text().contains("1")

}

}

Page 77: 2014 Jeeconf - Geb Spock

Вопросы?

Page 78: 2014 Jeeconf - Geb Spock

Всего хорошего, и спасибо за рыбу!