Бібліотеки для тестування (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 дозволяє розділяти код тестів та опис сторінок / екранів. Наприклад, при зміні сторінки / екрану вашого сайту / додатку достатньо буде переписати тільки відповідний клас, що описує цю сторінку / екран у тестах.
@Testpublic void setMusicInfo() { onMusicPage().setEra("90s"); onMusicPage().setArtist("Wu-Tang Clan"); onMusicPage().setSong("Protect Ya Neck")}
Builder Pattern
Builder Pattern - ваш помічник для конструкторів з різними конфігураціями. Цей патерн дозволяє створювати об’єкти покроково. Білдер дає можливість перевикористовувати код для отримання різних представлень об’єктів. Цей патерн використовується у нашому проекті.
@Testpublic 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 патерном.
@DataProviderpublic 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 практично немає, хіба що в причині дроблення коду. Насправді дізнався про цей шаблон проектування після того, як проект уже був створений. Інформації про нього в мережі немає або практично немає. Зустрів інформацію про нього на одній із доповідей.
@Testfun checkCredits(): Unit = step("Відображення елементів кредитного договору") { goToWallet() scrollToCategoryOnWallet(CREDITS) mockCredits("credits.json") clickOnCredits() checkCreditsElements()}
Robot Pattern
Новий патерн від відомого дядька Джека Уортона. Цей шаблон проектування добре підходить для Espresso тестів у зв’язці з Kotlin і схожий із патерном Builder.
Приклад базового використання:
@Testfun 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)
Приклади використання в тестах:
@Testfun successfulLogin() { login { setEmail("test@qaband.net") setPassword("secure123") clickLogin() matchSuccessLogin() }.home { // Тепер працюємо з HomeRobot checkWelcomeMessage() navigateToProfile() }}
@Testfun 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 (традиційний підхід)@Testfun loginTest() { LoginPage loginPage = new LoginPage() loginPage.setEmail("test@qaband.net") loginPage.setPassword("password") loginPage.clickLogin()
HomePage homePage = loginPage.getHomePage() assertTrue(homePage.isWelcomeMessageDisplayed())}
// Robot Pattern (сучасний підхід)@Testfun 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 синтаксис@Testfun `користувач може успішно залогінитися`() { Given("користувач знаходиться на сторінці входу") { openLoginScreen() }
When("користувач вводить правильні дані") { enterEmail("test@qaband.net") enterPassword("secure123") clickLoginButton() }
Then("користувач потрапляє на головну сторінку") { verifyHomeScreenIsDisplayed() verifyWelcomeMessage("Hello world!") }}
Переваги BDD:
-
Зрозуміло бізнесу - тести можуть читати нетехнічні спеціалісти
-
Документація - тести служать живою документацією
-
Співпраця - покращує комунікацію між командою
Fluent Interface Pattern
Патерн, який робить API більш читабельним та інтуїтивним.
// Приклад для API тестування@Testfun 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 тестування@Testfun testShoppingCart() { app.onProductPage() .selectProduct("MacBook Pro") .addToCart() .goToCart() .verifyItemCount(1) .proceedToCheckout() .fillShippingInfo("Київ, Хрещатик 1") .selectPaymentMethod("CARD") .completeOrder() .verifyOrderSuccess()}
Test Double Patterns
Сучасний підхід до створення тестових дублерів.
// Mock з Mockk (Kotlin)@Testfun 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)@Testfun 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 ) }}
// Використання в тестах@Testfun 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?
Для мобільного тестування:
-
Robot Pattern - для Espresso/XCUITest/Appium
-
BDD Given-When-Then - для acceptance тестів
-
Factory Pattern - для тестових даних
Для API тестування:
-
Builder Pattern - для складних запитів
-
Fluent Interface - для читабельності
-
Test Double - для ізоляції зовнішніх залежностей
Для веб-тестування:
-
Page Object Model - класичний, але робочий (Selenium, Appium WebView)
-
Screenplay - сучасна альтернатива POM
-
Data Provider - для тестування з різними даними
Основні принципи:
-
FIRST принцип: Fast, Independent, Repeatable, Self-validating, Timely
-
Pyramid тестування: Більше unit тестів, менше UI тестів
-
DRY принцип: Don’t Repeat Yourself - перевикористовуйте код
-
Readable тести: Код повинен читатися як документація
Хотілося б ще раз відзначити, що патернів існує “вагон і маленький візок”, що дозволяє знайти шаблон саме під свої потреби.
Рекомендую до ознайомлення цей ресурс, де все чудово описано і з прикладами коду на популярних мовах.