BDD and browser automation with Geb and Spock Данилюк Богдан Jeeconf | may 2014 | @bogdand
Aug 19, 2014
BDD and browser automation with Geb and Spock
Данилюк Богдан
Jeeconf | may 2014 | @bogdand
Главный вопрос жизни вселенной и всего такого
Главный вопрос жизни вселенной и всего такого
Как в любой момент быть уверенным в качестве текущей версии вашего
приложения и дать возможность людям понять что же оно делает?
Кто я?
● C Groovy / Grails начиная с 2008 года
● Успешно внедрили и используем Geb /
Spock
● - Groovy / Grails курс
● Первый разработчик в TransferWise,
сейчас 100+ человек
TransferWise
TransferWise
С чего все начиналось?
≈ 0% покрытие тестами
С чего все начиналось?
Smoke testing
● Тестировалось <10 страниц
● Время прохождения тестов ≈1 минута
● Обнаруженных ошибок - мало =)
Smoke testing
Gebvery groovy browser automation
Geb
● Groovy обертка вокруг Selenium 2
● Лицензия ASL2
● Текщая версия: 0.9.2
● Дружит с JUnit и Spock
● Groovy Page object
● http://gebish.org
Geb - WebDriver
● Мобильные браузеры:○ iPad○ iPhone○ Android○ Blackberry
● PhantomJS● HtmlUnit
Простой 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")
Простой 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")
Простой 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")
Простой 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")
Простой 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")
Простой 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")
Метод $()
$(«css selector», «index or range», «attribute / text matchers»)
Примеры
$("div") // все div элементы
$("div", 0) // первый div элемент
$("div", 0..2) // первых три div элемента
// Третий H2 элемент с текстом “Geb”
$("h2", 2, text: "Geb")
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")
Атрибуты
Поиск по атрибутам//<div foo="bar">
$("div", foo: "bar")
Атрибуты
Поиск по атрибутам//<div foo="bar">
$("div", foo: "bar")
Поиск по тексту//<div>foo</div>
$("div", text: "foo")
Навигация
Поиск по атрибутам//<div foo="bar">
$("div", foo: "bar")
Поиск по тексту//<div>foo</div>
$("div", text: "foo")
Поиск по регулярным выражениям//<div>foo</div>
$("div", text: ~/f.+/)
Вспомогательные функции
$("p", text: startsWith("p"))
$("p", class: contains("section"))
$("p", id: endsWith(~/\d/))
Больше примеров в документации
Относительный поиск
$("p").previous()
$("p").prevAll()
$("p").next()
$("p").parent()
$("p").siblings()
$("div").children()
$("p").nextAll(".listing")
Functional testing
Functional testing
● Время прохождения тестов - 20 минут
● Обнаруженных ошибок - уже больше =)
● Грядут редизайны веб страниц
GebСтраницы и модули
Page Object
class WikipediaPage extends Page {
static url = "https://www.wikipedia.org"
static at = { title == "Wikipedia" }
static content = {
search { $("#searchInput") }
}
}
Page Object
class WikipediaPage extends Page {
static url = "https://www.wikipedia.org"
static at = { title == "Wikipedia" }
static content = {
search { $("#searchInput") }
}
}
Page Object
class WikipediaPage extends Page {
static url = "https://www.wikipedia.org"
static at = { title == "Wikipedia" }
static content = {
search { $("#searchInput") }
}
}
Page Object
class WikipediaPage extends Page {
static url = "https://www.wikipedia.org"
static at = { title == "Wikipedia" }
static content = {
search { $("#searchInput") }
}
}
Page Object использование
Browser.drive {
to WikipediaPage
assert at(WikipediaPage)
search = "JeeConf"
}
Опциональный контент
class OptionalPage extends Page {
static content = {
errorMsg(required: false) { $("p.errorMsg") }
}
}
Динамический контент
class DynamicPage extends Page { static content = { errorMsg(wait: true) { $("p.errorMsg") } }}
Наследование Page Object
● Блоки content будут слиты
● Методы унаследуются
Повторное использование
● Блоки, повторяющиеся на многих страницах
● Блоки, повторяющиеся на одной странице
Объявление модуля
class NavBarModule extends Module {
static content = {
homePageLink(to: HomePage) { $("a#home") }
profileLink(to: ProfilePage) { $("a#profile") }
}
}
Использование модулей
class HomePage extends Page {
static content = {
navBarModule { module NavBarModule }
}
void goToProfilePage() {
navBarModule.profileLink.click()
}
}
Пример 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") }
}
}
Проблема поддержки
to HomePage
loginButton.click()
username = "user"
password = "password"
loginButton.click()
Page Object builder
● Подход предложен Craig Atkinson
● По умолчанию Geb делегирует вызовы методов текущей странице
● Билдер делает эти вызовы явными
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")
Вызов по цепочке
Реализация HomePage
class HomePage extends Page {
static content = {
loginButton(to: LoginPage, wait: true) {
$("#loginButton")
}
}
LoginPage clickLoginButton() {
loginButton.click()
return browser.page
}
}
Geb + Spock
Архитектура
App
Browser
WebDriverGebgeb-spock(Testing adapter)
Spok
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 }
}
}
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'
Проблемы
● Тестировались в основном длительные цепочки страниц
● Вся подготовка данных делалась через веб интерфейс
Некрасивый тест
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
}
Некрасивый тест
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
}
Use case тестирование
Чего мы хотим?
● Результат тестов не булевое значение
● Экранирование сценариев
● Упор на документирование
● Привлечение нетехнических специалистов для работы с тестами
Экранирование сценариев
Чего мы хотим достигнуть?
● Каждый тест готовит данные для себя
● Тест должен знать как можно меньше информации о внутренностях приложения
● Тест не должен ломаться при рефакторингах
Первая идея
А давайте обновлять базу?
Проблемы:
● Тесты знают много низкоуровневой информации о приложении
● Тысты очень чуствительны к изменениям в приложении
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.”
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()
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()
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()
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
}
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
}
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
}
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
}
Некрасивый тест
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
}
Красивый тест
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
Отчеты
Стандартный Grails отчет
Стандартный Grails отчет
Spock отчеты
Требования
● Логическая групировка тестов
● Возможность запуска группы тестов
● Человекочитаемая документация
Пример отчета
Spock отчеты
Решение
● Расширили Athaydes Spock Reports
○ Добавили группировку
○ Возможность запуска группы спецификаций
@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"
}
}
Что дальше?
● Тестирование верстки
● Ускорение тестов
● Переход на Gherkin (Cucumber) для избежания дублирования коментариев и кода
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")
}
}
Вопросы?
Всего хорошего, и спасибо за рыбу!