skip to content
Зміст

Бібліотеки для тестування (Espresso, Selenium, XCUITest, Rest Assured тощо) дозволяють тестам мати стабільну взаємодію з додатком, але без правильної реалізації ваші тести можуть стати важко керованими, нечитабельними та складними в підтримці. Саме для цього й існують патерни проектування в автоматизації тестування.

Відповіддю на запитання «Навіщо потрібні патерни?» буде:

  • Готові рішення. Не потрібно витрачати час, використовуйте вже готові рішення замість повторного винайдення велосипеда.

  • Стандартизація коду. Менше помилок у коді завдяки використанню уніфікованих рішень.

  • Зрозумілість. Навіть просто вимовивши назву патерну ви дасте зрозуміти колезі як все влаштовано в даному проекті (Микито, привіт 🙂).

Важливо розуміти, що патерн існує для того, щоб вирішувати проблеми вашого проекту. Якщо проблеми проекту якимось чином збігаються і накладаються на патерн, значить, вам варто його розглянути.

Патернів існує незліченна кількість, як приклад, книга - A Journey through Test Automation Patterns: One team’s adventures with the Test Automation Patterns (тільки тут описані 86 патернів)

Проблема патернів полягає в їх коректному використанні. Переходом на патерн Page Object не має слугувати те, що якийсь крутий спікер про нього розповів чи він на слуху. Потрібно розуміти, що використання патерну має в першу чергу вирішувати вашу проблему, а не бути просто модним.

Варто ще згадати, що хороших чи поганих патернів немає, як і найкращих, що явно незрозуміло з назви статті - 4 найкращі патерни проектування автоматизованого тестування (і ще 86) / Блог компанії OTUS. Онлайн-освіта / Хабр. На якомусь проекті може використовуватися один шаблон проектування, а на іншому проекті його використання взагалі не доцільне.

Page Object

Напевно найпоширеніший і найвідоміший патерн - Page Object. Цей патерн допомагає інкапсулювати роботу з окремими елементами сторінки, що дозволяє зменшити кількість коду та спростити його підтримку. Page Object дозволяє розділяти код тестів та опис сторінок / екранів. Наприклад, при зміні сторінки / екрану вашого сайту / додатку достатньо буде переписати тільки відповідний клас, що описує цю сторінку / екран у тестах.

@Test
public void setMusicInfo() {
onMusicPage().setEra("90s");
onMusicPage().setArtist("Wu-Tang Clan");
onMusicPage().setSong("Protect Ya Neck")
}

Builder Pattern

Builder Pattern - ваш помічник для конструкторів з різними конфігураціями. Цей патерн дозволяє створювати об’єкти покроково. Білдер дає можливість перевикористовувати код для отримання різних представлень об’єктів. Цей патерн використовується у нашому проекті.

@Test
public void setMusic(MusicEra musicEra,
String genre,
String solo_artist,
String jsonSchema) {
RequestModel model = new RequestModel.
music_genre(genre).
artist(solo_artist);
manager.getHelper(Music.class).
setMusic(musicEra, model).
verifyByJsonSchema(jsonSchema)
}

Data Provider

Data Provider - патерн, який дуже зручний за наявності однієї і тієї ж тестової логіки з різними тестовими даними. Також використовуємо цей шаблон у зв’язці з Builder патерном.

@DataProvider
public Object[][] positive() {
String musicGenre = manager.getHelper(GetMusic.class).getMusicGenre("RIGHT_HIP_HOP");
return new Object[][]{
(AFTER_1990, ARTIST, musicGenre, "Wu-Tang Clan", "Protect Ya Neck"),
(AFTER_1980, ARTIST, musicGenre, "N.W.A", "Fuck Tha Police"),
(AFTER_2000, ARTIST, musicGenre, "Eminem", "The Eminem Show,")
};
}

Інші поширені патерни проектування

Screenplay

Screenplay - це по факту просто підсумок рефакторингу Page Object. Являє собою більш деталізоване розбиття на елементи. Докладніше можете ознайомитися тут.

Page Element

Page Element - інкапсулює складність окремого UI-елемента. Прикладом може слугувати будь-яка таблиця як частина користувацького інтерфейсу. Особливо корисний в Appium для складних мобільних компонентів (списки, календарі, picker’и).

Steps pattern

У проекті Android автотестів ми використовуємо Steps Pattern. Саме він зручний для написання автотестів чітко за тест-кейсами, оскільки на кожен крок кейсу у вас буде виконуватися окремий метод. Це дозволяє кожному, хто подивиться на тест, зрозуміти які дії виконуються на даному етапі і яка їх послідовність. Відмінностей між Page Object практично немає, хіба що в причині дроблення коду. Насправді дізнався про цей шаблон проектування після того, як проект уже був створений. Інформації про нього в мережі немає або практично немає. Зустрів інформацію про нього на одній із доповідей.

@Test
fun checkCredits(): Unit = step("Відображення елементів кредитного договору") {
goToWallet()
scrollToCategoryOnWallet(CREDITS)
mockCredits("credits.json")
clickOnCredits()
checkCreditsElements()
}

Robot Pattern

Новий патерн від відомого дядька Джека Уортона. Цей шаблон проектування добре підходить для Espresso тестів у зв’язці з Kotlin і схожий із патерном Builder.

Приклад базового використання:

@Test
fun loginMissingPassword() {
login {
setEmail("mail@example.com")
clickLogin()
matchErrorText(string(R.string.missing_fields))
}
}

Детальна реалізація Robot Pattern

Створення базового робота:

class LoginRobot {
fun setEmail(email: String): LoginRobot {
onView(withId(R.id.email_input))
.perform(typeText(email), closeSoftKeyboard())
return this
}
fun setPassword(password: String): LoginRobot {
onView(withId(R.id.password_input))
.perform(typeText(password), closeSoftKeyboard())
return this
}
fun clickLogin(): LoginRobot {
onView(withId(R.id.login_button))
.perform(click())
return this
}
fun matchErrorText(@StringRes stringRes: Int): LoginRobot {
onView(withId(R.id.error_text))
.check(matches(withText(stringRes)))
return this
}
fun matchSuccessLogin(): HomeRobot {
onView(withId(R.id.welcome_text))
.check(matches(isDisplayed()))
return HomeRobot()
}
}
// Extension function для зручності
fun login(func: LoginRobot.() -> Unit) = LoginRobot().apply(func)

Приклади використання в тестах:

@Test
fun successfulLogin() {
login {
setEmail("test@qaband.net")
setPassword("secure123")
clickLogin()
matchSuccessLogin()
}.home {
// Тепер працюємо з HomeRobot
checkWelcomeMessage()
navigateToProfile()
}
}
@Test
fun loginValidationFlow() {
login {
clickLogin()
matchErrorText(R.string.fields_required)
setEmail("invalid-email")
clickLogin()
matchErrorText(R.string.invalid_email)
setEmail("test@qaband.net")
setPassword("123")
clickLogin()
matchErrorText(R.string.weak_password)
}
}

Переваги Robot Pattern:

  • Fluent Interface - читається як звичайна мова
  • Chainable Methods - можна об’єднувати дії в ланцюжки
  • Platform Agnostic - працює з будь-якими UI фреймворками (Espresso, Appium, XCUITest)
  • Readability - тести читаються як користувацькі сценарії
  • Reusability - роботи можна переналаштовувати та комбінувати

Порівняння з Page Object:

// Page Object (традиційний підхід)
@Test
fun loginTest() {
LoginPage loginPage = new LoginPage()
loginPage.setEmail("test@qaband.net")
loginPage.setPassword("password")
loginPage.clickLogin()
HomePage homePage = loginPage.getHomePage()
assertTrue(homePage.isWelcomeMessageDisplayed())
}
// Robot Pattern (сучасний підхід)
@Test
fun loginTest() {
login {
setEmail("test@qaband.net")
setPassword("password")
clickLogin()
matchSuccessLogin()
}.home {
checkWelcomeMessage()
}
}

Robot Pattern особливо крутий для мобільних тестів, де потрібно багато послідовних дій. Замість 10 рядків коду отримуєте 3, а читабельність збільшується в рази


Сучасні патерни тестування (2025)

BDD (Behavior Driven Development)

BDD - це підхід, який зосереджує увагу на поведінці додатка з точки зору користувача. Використовує природну мову для опису тестів.

Структура Given-When-Then:

// Cucumber/Gherkin синтаксис
@Test
fun `користувач може успішно залогінитися`() {
Given("користувач знаходиться на сторінці входу") {
openLoginScreen()
}
When("користувач вводить правильні дані") {
enterEmail("test@qaband.net")
enterPassword("secure123")
clickLoginButton()
}
Then("користувач потрапляє на головну сторінку") {
verifyHomeScreenIsDisplayed()
verifyWelcomeMessage("Hello world!")
}
}

Переваги BDD:

  • Зрозуміло бізнесу - тести можуть читати нетехнічні спеціалісти

  • Документація - тести служать живою документацією

  • Співпраця - покращує комунікацію між командою

Fluent Interface Pattern

Патерн, який робить API більш читабельним та інтуїтивним.

// Приклад для API тестування
@Test
fun testUserRegistration() {
api.users()
.create()
.withName("imbirovsky")
.withEmail("test@qaband.com")
.withRole("QA_ENGINEER")
.expect()
.statusCode(201)
.responseTime(lessThan(2000))
.body("id", notNullValue())
.body("email", equalTo("test@qaband.com"))
}
// Або для UI тестування
@Test
fun testShoppingCart() {
app.onProductPage()
.selectProduct("MacBook Pro")
.addToCart()
.goToCart()
.verifyItemCount(1)
.proceedToCheckout()
.fillShippingInfo("Київ, Хрещатик 1")
.selectPaymentMethod("CARD")
.completeOrder()
.verifyOrderSuccess()
}

Test Double Patterns

Сучасний підхід до створення тестових дублерів.

// Mock з Mockk (Kotlin)
@Test
fun testUserService() {
val userRepository = mockk<UserRepository>()
val emailService = mockk<EmailService>()
every { userRepository.save(any()) } returns User(id = 1, email = "test@qaband.com")
every { emailService.sendWelcome(any()) } just Runs
val userService = UserService(userRepository, emailService)
val result = userService.registerUser("test@qaband.com", "password")
result.shouldNotBeNull()
result.id shouldBe 1
verify { userRepository.save(any()) }
verify { emailService.sendWelcome(any()) }
}
// Test Double для мобільних сервісів (Appium)
@Test
fun testLocationService() {
val mockLocationService = mockk<LocationService>()
every { mockLocationService.getCurrentLocation() } returns
Location(latitude = 50.4501, longitude = 30.5234) // Київ
val app = AppUnderTest(mockLocationService)
// Тестуємо без реального GPS
app.findNearbyRestaurants()
verify { mockLocationService.getCurrentLocation() }
}

Factory Pattern для тест-даних

// Створення тестових об'єктів
object TestDataFactory {
fun createUser(
name: String = "Test User",
email: String = "test@qaband.com",
role: UserRole = UserRole.USER
): User {
return User(
id = Random.nextLong(),
name = name,
email = email,
role = role,
createdAt = Instant.now()
)
}
fun createProduct(
title: String = "Test Product",
price: BigDecimal = BigDecimal("99.99"),
category: String = "Electronics"
): Product {
return Product(
id = UUID.randomUUID(),
title = title,
price = price,
category = category
)
}
}
// Використання в тестах
@Test
fun testUserCreation() {
val user = TestDataFactory.createUser(
name = "imbirovsky",
email = "imbirovsky@qaband.com"
)
userService.save(user)
val savedUser = userService.findByEmail("imbirovsky@qaband.com")
savedUser.shouldNotBeNull()
savedUser.name shouldBe "imbirovsky"
}

Антипатерни в тестуванні

Розглянемо найпоширеніші антипатерни (те, як НЕ треба робити):

Test Ice Cream Cone

Погано:

// 90% UI тестів, 10% unit тестів
@Test fun testCompleteUserFlowThroughUI() {
// 50+ рядків UI взаємодій для простої перевірки логіки
ui.openApp()
.navigateToLogin()
.login("user", "pass")
.navigateToProfile()
.editProfile()
.changeEmail("new@email.com")
.save()
.logout()
.login("user", "pass")
.verifyEmailChanged("new@email.com")
}

Добре:

// Unit тест для бізнес-логіки
@Test fun testEmailValidation() {
val validator = EmailValidator()
validator.isValid("test@qaband.net") shouldBe true
validator.isValid("invalid-email") shouldBe false
}
// Простий UI тест тільки для критичного потоку
@Test fun testLoginFlow() {
ui.login("user", "pass")
.verifySuccessfulLogin()
}

Testing Implementation Instead of Behavior

Погано:

@Test fun testUserServiceCallsRepository() {
// Тестуємо реалізацію, а не поведінку
userService.saveUser(user)
verify(userRepository).save(user)
verify(emailService).send(any())
verify(auditService).log(any())
}

Добре:

@Test fun testUserRegistration() {
// Тестуємо поведінку
val result = userService.registerUser("test@qaband.com", "password")
result.shouldNotBeNull()
result.email shouldBe "test@qaband.com"
result.isActive shouldBe true
}

Happy Path Only Tests

Погано:

@Test fun testLogin() {
// Тільки успішний сценарій
ui.login("valid@email.com", "validPassword")
.verifySuccess()
}

Добре:

@Test fun testLogin() {
// Покриваємо різні сценарії
ui.login("valid@email.com", "validPassword")
.verifySuccess()
}
@Test fun testLoginWithInvalidCredentials() {
ui.login("invalid@email.com", "wrongPassword")
.verifyErrorMessage("Invalid credentials")
}
@Test fun testLoginWithEmptyFields() {
ui.login("", "")
.verifyErrorMessage("Please fill all fields")
}

Slow Tests

Погано:

@Test fun testUserCreation() {
Thread.sleep(5000) // Чому?!
// Або використання реальних API замість моків
val response = realApiClient.createUser(user)
response.statusCode shouldBe 200
}

Добре:

@Test fun testUserCreation() {
// Швидкі unit тести з моками
val mockClient = mockk<ApiClient>()
every { mockClient.createUser(any()) } returns Response(200, user)
val result = userService.createUser(user)
result.success shouldBe true
}

Якщо ваші тести працюють довго - це сигнал, що щось не так. Швидкі тести = щасливі розробники 🤭

Test Data Pollution

Погано:

@Test fun testUserSearch() {
// Створюємо дані в базі без очищення
database.insert(User(name = "Test User"))
val results = userService.search("Test")
results.size shouldBe 1
// Дані залишаються в базі і впливають на інші тести!
}

Добре:

@Test fun testUserSearch() {
// Ізольовані тестові дані
val testUser = TestDataFactory.createUser(name = "Test User")
database.insert(testUser)
val results = userService.search("Test")
results.size shouldBe 1
// Очищення після тесту
database.cleanup()
}

Підсумки та рекомендації

Які патерни вибрати у 2025?

Для мобільного тестування:

  1. Robot Pattern - для Espresso/XCUITest/Appium

  2. BDD Given-When-Then - для acceptance тестів

  3. Factory Pattern - для тестових даних

Для API тестування:

  1. Builder Pattern - для складних запитів

  2. Fluent Interface - для читабельності

  3. Test Double - для ізоляції зовнішніх залежностей

Для веб-тестування:

  1. Page Object Model - класичний, але робочий (Selenium, Appium WebView)

  2. Screenplay - сучасна альтернатива POM

  3. Data Provider - для тестування з різними даними

Основні принципи:

  • FIRST принцип: Fast, Independent, Repeatable, Self-validating, Timely

  • Pyramid тестування: Більше unit тестів, менше UI тестів

  • DRY принцип: Don’t Repeat Yourself - перевикористовуйте код

  • Readable тести: Код повинен читатися як документація


Хотілося б ще раз відзначити, що патернів існує “вагон і маленький візок”, що дозволяє знайти шаблон саме під свої потреби.

Рекомендую до ознайомлення цей ресурс, де все чудово описано і з прикладами коду на популярних мовах.

Обговорення
Вхід через GitHub
Завантаження коментарів...