Автоматизоване тестування застосунку на React Native та інтеграція в СI/CD
Привіт, я Інна — Automation QA Lead в EdTech-стартапі Mate academy, який допомагає будувати кар’єру в ІТ. Наша команда займається розробкою власної навчальної платформи, а також мобільного застосунку для Android та iOS на базі React Native.
Раніше я вже писала про наш досвід міграції E2E тестування з Cypress на Playwright, а цього разу розповім про запровадження автоматизованого тестування для мобільного застосунку та його інтеграцію в СI/CD flow. А також поділюся технічними моментами реалізації та думками, чому це варто робити.
Чому ми вирішили писати автотести для мобайл
Для початку варто зазначити, що ми робимо щоденні релізи для нашої LMS-платформи. Завдяки такому підходу наші користувачі (студенти) одразу починають використовувати нові фічі та швидко дають нам зворотний зв’язок. Маємо можливість швидко тестувати нові ідеї, вже на наступний день бачити результати в аналітиці та розуміти, чи рухаємося в правильному напрямку. До речі, нещодавно моя колега Аня Ткаченко розповіла детальніше про процес побудови аналітики та data-driven підхід до побудови продуктів. Якщо комусь цікава ця тема, рекомендую ознайомитися у блозі.
Коли працюєш зі щоденними деплоями, наявність якісного покриття автотестами — необхідна умова стабільних релізів та відсутності критичних помилок у продакшн. Це добре працює для вебпродуктів, а як реалізувати щоденні або хоча б достатньо часті релізи мобільного застосунку?
Ми запустили мобільний застосунок як експеримент, тому на початку тестували тільки вручну. Це мало сенс, бо ми не знали напевне, наскільки корисним буде цей проєкт. Але з часом побачили, що застосунок набуває популярності, а отже — варто підтримувати його надалі. Постало питання, як зробити релізи частими та стабільними. Кількість функціоналу значно зросла і наша QA команда вже потребувала півтора-два дні на те, щоб пройтися по регресійним сценаріям вручну перед релізом. А оскільки застосунок не тестувався автоматично, ми часто знаходили регресійні баги, що вимагали фіксу та відкладали реліз ще на день-два.
Стало очевидним, що нам потрібні автотести як частина CI/CD процесу. Першим кроком до цього став вибір фреймворку для мобайл-автоматизації.
Вибір інструментів
У своїй минулій статті я вже писала про загальні критерії вибору інструментів для автоматизації. Обираючи інструменти для мобайл-тестування, ми керувалися схожим списком:
- Мова програмування. Ми вже мали наявний фреймворк для Web на Playwright & TypeScript, тому цілком логічно, що нам було зручно реалізувати новий проєкт, використовуючи TypeScript.
- Набір можливостей інструменту щодо взаємодії з мобільними застосунками.
- Наявні інструменти для організації та запуску тестів.
- Репортер. Для нас було важливо інтегрувати новий проєкт з Allure, оскільки ми вже використовували його для всіх інших наших тестів і було зручно мати все в одному місці.
- Можливість паралелізації тестів. Можливість автоматичного шардингу.
- Можливість автоматичних перезапусків та конфігурація їхньої кількості.
- Можливість підтримувати одні й ті ж тести для Android та iOS.
- Потенційна можливість підтримувати нативні застосунки Android та iOS, якщо в майбутньому ми вирішимо перейти до їх розробки.
- Можливість ранити тести на емуляторах.
- Швидкість виконання тестів.
- Наявність великого комʼюніті, що використовує цей інструмент. Якість документації. Це важливо для легшого пошуку розв’язання проблем, з якими можна зіткнутися під час використання.
Дослідивши доступні та популярні зараз інструменти для мобайл-автоматизації на TypeScript, ми вирішили детальніше розглянути Appium та Detox.
Appium — це широко відомий open source фреймворк для UI-автоматизації на багатьох платформах, зокрема і для iOS & Android тестів. Appium можна назвати «black-box testing» інструментом, оскільки він допомагає якомога точніше імітувати в тестах те, що відбувається при реальній взаємодії юзера з UI. Через таку властивість він дає можливість автоматизувати тести як для мобільних застосунків, написаних на React Native, так і для нативних застосунків. Appium підтримує більшість популярних мов програмування, серед яких є TypeScript.
Detox — також досить відомий інструмент, створений безпосередньо для тестування застосунків написаних на React Native. Наразі підтримує iOS & Android. Самі автори називають його «gray-box testing» фреймворк. Адже він має вбудовану синхронізацію зі станом застосунку та доступ до його елементів. Detox відстежує асинхронні операції в застосунку, що допомагає підвищити стабільність роботи тестів. З Detox ви можете писати тести тільки на JavaSrcipt або TypeScript.
Proof of Concept (POC)
Наступним кроком після дослідження наявних інструментів стало створення Proof of Concept.
На мою думку, дуже важливо спробувати на практиці різні інструменти до того, як приймати рішення, що взяти за основу вашого фреймворку. Якщо маєте час та можливість, не обмежуйтесь одним тулом, а спробуйте два або навіть три. У вас може виникнути побоювання, що це займе багато часу, але краще виділити час одразу, ніж постфактум пожалкувати про неоптимальний вибір. Спробувавши кілька інструментів на практиці, ви розширюєте свою експертизу і приймаєте більш зважене рішення. В інтернеті можна знайти багато експертних порад щодо того, який інструмент кращий. Але завжди є ймовірність, що вони не будуть релевантними для вашого випадку, оскільки пріоритети та вподобання можуть відрізнятися.
Отже, ми зробили POC для двох інструментів:
- Detox
- Appium + Webdriver.io
На практиці це означало, що для кожного з тулів ми зробили мініпроєкт, який містив:
- Конфігураційні файли.
- Базові класи з селекторами та методами для роботи зі скринами.
- E2E тестовий сценарій, який у нашому випадку містив:
- старт сесії апки на Android-емуляторі;
- перехід на сторінку Login;
- введення даних юзера та логін;
- прямий перехід на скрин для роботи з чатом для тестування роботи з deep links (про них трохи згодом);
- перевірку потрібних елементів на скрині з чатом.
- Інтеграція Allure репортів зі степами:
- відображення степів та пре-пост кондішенів у репортах
- Лінкування скриншотів та відео для зафейлених тестів.
- Лінкування логів.
- Робота з ретраями.
- Замір середнього часу на виконання сценарію.
Щодо вибору тестового сценарію для POC. Моя рекомендація — взяти повноцінний сценарій з тестового набору, який буде репрезентувати приклад того, що ви збираєтеся покривати автотестами. Таким чином ви зможете краще зрозуміти як працює інструмент, скільки часу займає тестовий сценарій та як організувати тестовий код, враховуючи особливості даного інструменту.
На реалізацію кожного з інструментів було вкладено приблизно по 2 дні, тож насправді випробувати кілька тулів не так довго, як може здатися.
Спробувавши обидва інструменти та зібравши дані, ми зробили документ, який описував виявлені особливості обох інструментів у формі таблички. Після чого поділилися ним з усією командою для прийняття фінального рішення. Також документ містив посилання на PR з обома варіантами фреймворку в github. Табличка виглядала приблизно ось так:
Ми зробили простий тест швидкості, заранивши сценарій з кожним інструментом в циклі та взявши середнє від прогонів. Результатом тесту була перевага Detox на 25%. Важливо додати, що це приблизна оцінка. Якщо ви хочете дуже точно оцінити різницю у швидкості, потрібно взяти кілька сценаріїв різної тривалості, ранити їх в циклі та забезпечити однакові умови прогонів в ізольованому середовищі. Та який би не був швидкий інструмент сам по собі, фінальний результат залежить від кількох факторів, включно з ефективністю технічної реалізації фреймворку. Про інструменти скорочення часу тестів розповім нижче.
Тож основною перевагою Detox є його швидкість. Але gray-box природа Detox має як плюси, так і мінуси. Оскільки вона дає можливість покривати меншу кількість сценаріїв та вимагає інтеграції й ребілду разом з кожним білдом застосунку. Appium — повільніший, але універсальніший тул, який точніше відтворює дії юзера і відповідно дає можливість більше довіряти результатам тестів. Також він більш звичний для інженерів, які раніше вже працювали з Playwright, Selenium і подібними фреймворками. Ще одним плюсом Appium для нас стало те, що він «з коробки» підтримує роботу з Allure-репортером, який ми вже використовували для всіх інших тестів.
Визначившись з пріоритетами, ми вирішили рухатися далі з Appium + Webdriver.io.
Інтеграція до СI/CD
Наступним важливим етапом була інтеграція мобільних автотестів до нашого СI/CD процесу.
Наш основний СI/CD pipeline для платформи реалізований в GitHub actions і запускається на AWS EC2 машинах в докері. Для мобайл-тестування було логічним спробувати втілити схожий підхід.
Спочатку була спроба підняти Android-емулятор від Google на стандартних EC2 машинах. Як виявилося, вони не достатньо потужні для стабільної роботи емулятора. Альтернативою може бути використання для цього потужних bare-metal інстансів, але це вже буде досить дорого. Варто розглядати їх тільки коли ви хочете ранити дуже велику кількість тестів і маєте на це кошти.
Ви також можете реалізувати автоматизоване тестування на реальних девайсах в клауді (наприклад, за допомогою BrowserStack), але ми не розглядали цей варіант через дуже високу ціну таких сервісів на масштабі наших тестових запусків.
Фінально ми реалізували запуск тестів за допомогою Genymotion Сloud образів з Android для EC2 інстансів. Цей сетап дорожчий, ніж використання емуляторів Google, але він набагато стабільніший та швидший.
Як ми це імплементували — є головний тест-раннер (ec2 instance), на якому:
- запускаються докер-контейнери з бекендом, до якого буде коннектитися апка;
- інсталюється тестовий фреймворк;
- трігериться старт окремого ec2 з android образом від genymotion;
- встановлюється коннект до цього ec2 за допомогою тунелю;
- запускається Webdriver.io, який конектиться через adb до машини з емулятром, встановлює наш застосунок та запускає тести.
Всі мобайл-тести запускаються на кожен пул-реквест зі змінами в коді мобільного застосунку або в коді самих тестів. Тож коли вирішуємо зробити реліз бранчу з новою версією апки, можемо бути впевнені: весь основний функціонал робочий. А ще не витрачаємо багато часу на ручне регресійне тестування.
Технічні особливості реалізації
У процесі роботи над фреймворком та інтеграцією його до CI/CD ми тримали у фокусі швидкість та стабільність тестів. Для забезпечення цього ми реалізували:
- Sign-in за допомогою deep links.
- Навігацію між скринами за допомогою deep links.
- Використання
ID-елементів як основного локатора для взаємодії з mobile UI. - Повторне використання кодової бази Web UI E2E для підготовки тестових даних.
Sign-in за допомогою deep links
Одним з перших важливих завдань при побудові мобайл-автоматизації була реалізація швидкого логіну до апки. Оскільки тести мають бути незалежними і використовувати індивідуальні дані для забезпечення стабільності та уникнення flaky-поведінки, потрібно щоб кожен тест працював з окремим залогіненим юзером. У випадку з web-UI автотестами це можна реалізувати за допомогою API-запиту, який одразу залогінить юзера та отримає авторизаційний токен, який потім зберігається і використовується.
У випадку з мобільним застосунком нам потрібен був аналог такої поведінки. Ми реалізували це за допомогою deep links.
У контексті мобільних застосунків deep link означає використання уніфікованого ідентифікатора ресурсу (URI), який посилається на конкретне місце в мобільній програмі, а не просто запускає програму.
Отже, в коді мобільної апки ми додали deep link, при переході за яким юзер автоматично логіниться та потрапляє на головний скрин. У тестовому коді це виглядає наступним чином:
У файлі з константами додаємо роут:
export const ROUTES = { signIn: (email: string, password: string): { index: string } => ({ index: `/sign-in/${email}/${password}`, }), }
Реалізовуємо метод для навігації по скринам та метод логіну за допомогою deep link:
async navigateTo(url: string): Promise<void> { await driver.navigateTo(`${this.baseUrl}${url}`); } async signInWithDeepLink(email: string, password: string): Promise<void> { await step('Sign in user with deep link', async () => { await this.navigateTo(ROUTES.signIn(email, password).index); }); }
Далі реалізовуємо метод, який логінить юзера за допомогою deep link та очікує на завантаження скрина, що має відкритися для відповідного юзера.
export async function signInUserWithDeepLink( user: User, screenToWaitFor?: BaseScreen, ): Promise<void> { await step('Sign in with DeepLink and wait for screen opened', async () => { const welcomeScreen = new WelcomeScreen(); await welcomeScreen.assertOpened(); await welcomeScreen.signInWithDeepLink(user.email, user.password); if (screenToWaitFor) { await screenToWaitFor.assertOpened(); } else { const coursesScreen = new CoursesScreen(); await coursesScreen.assertOpened(); } }); }
Далі використовуємо метод «signInUserWithDeepLink» в beforeTest для того, щоб залогінити юзера для відповідного сценарію. Тобто в тесті ми пропускаємо перехід на сторінку логіну та ввід даних для логіну. Виглядає це так:
У кожному тесті це економить нам в приблизно
Навігація між скринами за допомогою deep links
Аналогічно до описаного підходу із sign-in скрином, ми використовуємо deep links також і для інших скринів. У веб-автоматизації є можливість перейти до будь-якої вебсторінки з потрібним функціоналом на початку тесту та почати його звідти. У мобайл-автоматизації deep links забезпечують той самий функціонал.
Наприклад, якщо ми маємо ось таку вкладеність скринів:
Замість того, щоб проходитися по всім скринам, ми реалізували deep links:
- /courses/{courseId}
- /courses/{courseId}/{moduleId}
- /courses/{courseId}/{moduleId}/{topicId}/video
- /courses/{courseId}/{moduleId}/{topicId}/theory
- /courses/{courseId}/{moduleId}/{topicId}/practice
Залежно від потреби ми можемо одразу відкрити потрібний скрин, оминаючи навігацію по всім вкладенням, і розпочати тестовий сценарій на потрібному скрині.
Звісно, навігацію між скрнами також потрібно тестувати. Тому це реалізується в окремих тестових сценаріях. Але не повторюється багаторазово там, де це не є метою тестового сценарію.
Такий підхід також економить дуже багато часу в тестах.
Використання ID елементів для React Native mobile app
Для забезпечення стабільності тестів та легкого пошуку елементів ми стараємося використовувати унікальні ID елементи всюди, де це можливо. Нові айді для елементів скринів QA-інженери найчастіше додають самостійно. Для цього довелося розібратися в кодовій базі мобільного застосунку, але це дуже корисно з погляду швидкості розробки. Оскільки QA-команда може не чекати доки девелопери додадуть новий айдішник.
Варто зазначити, що для Android та iOS-застосунків, написаних на React Native, вам знадобляться різні типи айдішників. Для iOS можна стандартно використовувати testID атрибут. Але через особливості реалізації Android-застосунків на React Native доведеться також використовувати accessibilityLabel атрибут. Це дозволить Appium завжди знаходити елемент через його ’accessibility id’ локатор-стратегію. Для того, щоб не дублювати два айдішники всюди, де вони потрібні в коді апки, ми зробили метод, який одразу додає їх обох.
export default function addTestIds(id: string) { return { accessibilityLabel: id, testID: id }; }
Використовуючи його, можна одразу додати всі потрібні атрибути до відповідного компонента.
<Component {...addTestIds('foo')} />
Повторне використання інфраструктури, створеної для Web E2E тестів
Важливою метою під час створення фреймворку для нас було повторне використання вже наявної тестової інфраструктури.
Раніше ми створили фреймворк для тестування нашої вебплатформи за допомогою Playwright & TypeScript. Тож вже мали чимало тестів, а також достатньо ґрунтовну тестову інфраструктуру для роботи з API та базою. Логічно було не писати все заново для мобільних тестів. Це відкривало можливості набагато швидшої реалізації сценаріїв для мобайл.
Загалом, коли ви хочете реалізувати рішення такого типу, у вас є кілька шляхів.
- Винести частину коду, яка є спільною для різних фреймворків, в окремий проєкт, запакувати його та використовувати як залежність у потрібних проєктах. Але, як на мене, з таким підходом важко працювати, якщо вам постійно потрібно вносити зміни і в кор-бібліотеку, і у фреймворк. Це постійний ребілд та апдейт залежностей і для тестового фреймворку виглядає як надто складне рішення.
- Створити спільний проєкт для різних типів тестів, розділяючи код, потрібний для кожного з типів автотестів, та спільний код. Основною особливістю тут є одночасне використання двох різних автотулів на одній кодовій базі. Ми вирішили піти цим шляхом. Для цього нам довелося внести кілька змін в наявні класи та методи, щоб зробити їх цілком незалежними від Playwright API клієнта.
Організація проєкту виглядає так:
Основна особливість: є абстрактний клас APIClientBase, від якого наслідуються клієнти — ApiClientPlaywright та ApiClientAppium, що реалізують всі потрібні методи взаємодії з апішкою в кожному з тулів. При запуску тих чи інших тестових наборів використовується відповідний клієнт, а от всі класи, які реалізують методи роботи з апішкою конкретних сервісів, є загальними для обох тулів і при ініціалізації отримують потрібний клієнт як вхідний параметр.
Ще кілька порад
З мого досвіду роботи над автотестами для мобільного застосунку хочеться поділитися ще кількома порадами.
Одразу додам, що інструментарій для мобільної автоматизації наразі значно відстає від вебавтоматизації (тихо мрію, що автори Playwright візьмуться і за цю нішу — хто спробував, той зрозуміє :) ). Тести тут значно повільніші і не такі стабільні, як хотілося б. І все ж, не буду списувати все на гірку долю, а скажу, що потрібно грамотно продумувати сценарії та вкладатися в оптимізацію й фікс нестабільної поведінки.
Якщо ви використовуєте Appium, не варто писати дуже короткі сценарії, адже для кожного spec-файлу Appium запускає нову сесію, відповідно очищає дані застосунку та заново його стартує. Кожен такий перезапуск займає дорогоцінний час, і це часто значно довше, ніж перезапустити сторінку браузера (для порівняння).
Але не варто також писати надто довгі сценарії. По-перше, тест повинен мати чітку мету (що він перевіряє). Ви маєте легко розуміти, що саме пішло не так у випадку падіння. Крім того, якщо ви використовуєте ре-рани, довгі тести будуть займати ще більше часу загалом.
Також використовуйте конфігураційні можливості фреймворків на максимум для того, щоб оптимізувати проходження тестів саме для вашої задачі.
Configurations based on environment type
У нашому випадку середовище розробки та середовище запуску тестів в СI достатньо відрізняються. Локально тести розробляються на лептопі, де додатково до тестового середовища може відбуватися ще купа інших процесів, тож часто він повільніший ніж CI.
Для того, щоб не змінювати постійно конфігураційний файл та не дублювати його в кілька копій, ми використовуємо змінну середовища ENV_TYPE і, залежно від неї, вибираємо потрібний конфіг.
waitforTimeout: process.env.ENV_TYPE === 'ci' ? 5000 : 10000,
Fail-fast
Ми фейлимо весь тестовий запуск, якщо значна кількість тестів вже впала. Оскільки ми запускаємо тести на кожному PR, немає сенсу витрачати гроші на прогон всіх тестів, якщо очевидно, що вони зламані змінами. У Webdriver.io за це відповідає конфігурація змінної bail в файлі wdio.conf.ts:
bail: process.env.ENV_TYPE === 'ci' ? 5 : 0,
Re-runs and flaky tests
Також при запуску на CI ми допускаємо 1 ре-ран. На випадок, якщо перший раз тест впав через якісь інфраструктурні проблеми. У webdrver.io за це відповідає конфігурація змінної specFileRetries в файлі wdio.conf.ts.
specFileRetries: process.env.ENV_TYPE === 'ci' ? 1 : 0,
Але при цьому варто наголосити, що ми стараємося не толерувати флекі-тести в щоденних ранах. Ми маємо дашборд, в якому аналізуємо всі ре-рани за останні дні, щоб виправити нестабільну поведінку тестів.
Timeout for test scenario execution
Як я писала вище, ми стараємося знайти золоту середину в сценаріях — не писати їх надто довгими чи надто короткими. Що рівномірніші сценарії за часом, то легше їх буде розподілити по машинах, якщо ви використовуєте sharding. Тож ми обмежуємо час на кожен сценарій, і якщо це триває довше очікуваного — він фейлиться. У wdio.conf.ts за це відповідає конфігурація змінної timeout — в нашому випадку для тест-раннера mocha.
mochaOpts: { timeout: process.env.ENV_TYPE === 'ci' ? 35000 : 60000, },
Exclude skipped tests from the test run
Іноді виникає потреба тимчасово скіпнути котрийсь тест з рану. Наприклад, через flaky-поведінку, яку ви хочете пофіксити. Це робиться за допомогою звичного всім додавання describe.skip() методу. При цьому Webdriver тест-раннер все одно запускає нову сесію мобільної апки. А потім, розуміючи, що тест скіпнутий, закриває її. Виходить, витрачається непотрібний час на старт та стоп апки. Також це може вплинути на стабільність наступного тесту. Щоб уникнути такої поведінки, перед запуском всіх тестів ми парсимо тестові файли, які містять .skip, а потім додаємо список цих тестів до відповідного конфігу Webdriver.io:
exclude: skippedSpecFilesList,
Результати та теперішній стан
На цей момент наша команда покрила автотестами більшість P0/P1 сценаріїв роботи з мобільною апкою. Сценарії нижчого пріоритету ми свідомо вирішили не покривати, оскільки така автоматизація вимагає значних коштів. Крім того, частина P0/P1 сценаріїв залишилась непокритою через технічні обмеження. Наприклад, робота з embedded video з інших платформ.
Запускаючи наявні тести на кожен пул-реквест, ми маємо можливість дуже швидко відловити регресійні баги і таким чином бути впевненішим, що нічого не було зламано кардинально.
Але чи можна зовсім відмовитися від мануального тестування, маючи автотести для мобільної апки? На мою думку, це не дуже реально, оскільки тести переважно ранять на емуляторах (тестові прогони на девайсах в клауді можливі, але дуже дорогі). Існують випадки, коли поведінка реальних девайсів відрізняється. Ще є функціонал, який неможливо покрити автотестами. Також ми поки запускаємо тести тільки на Android-емуляторі, але не на iOS. У випадку з мобільними апками на React Native поведінка на iOS не буде сильно відрізнятися, але все ж ми натрапляли на баги, характерні тільки для однієї з платформ.
Може виникнути питання, чи варто тоді взагалі вкладатися в автоматизацію тестів для мобільних застосунків, якщо ми не можемо повністю позбутися ручного тестування? На мою думку, так. Це точно варто використовувати, оскільки ви зможете робити релізи частіше, значно зменшивши час на регресію.
Наприклад, створивши покриття автотестами, ми маємо змогу робити два види релізів:
- релізи нових фіч або просто релізи з великою кількістю змін. Вони робляться за стандартним флоу, коли ви білдите нову версію апки та пушите її в Google Play Store та App Store;
- так звані код-пуші (code-pushes) — невеликі зміни, баг фікси, для яких не потрібно білдити та пушити новий білд в store. Натомість зміни одразу потрапляють до юзерських девайсів.
У першому випадку, коли потрібно протестувати нові фічі та перевірити, що старі працюють як і раніше, ми робимо ручне регресійне тестування на основі P0/P1 сценаріїв. Автоматизація дала нам можливість скоротити час на ручний прогон приблизно з
Висновок
Реалізувавши проєкт для мобайл автотестування, можу сказати: це не найпростіша задача. Вона потребує значних зусиль і технічних навичок, якщо порівнювати, наприклад, з Web UI-автоматизацією. Але якщо ви маєте на меті робити релізи часто і стабільно, створення мобайл автотестів та їх інтеграція до CI сприятимуть цьому.
Важливим результатом автоматизації для нас стала можливість швидше реагувати на потреби користувачів (студентів) не лише на вебплатформі, а і в мобайл застосунку. Ми вкладаємося в доступність нашої програми на різних платформах, щоб створити гнучкий навчальний процес. А швидкі релізи дозволяють деліверити нові фічі та оновлення, які й забезпечують цю гнучкість. А також комфортне та, головне — ефективне навчання.
Сподіваюся, описане в цьому блозі допоможе комусь з читачів реалізувати автоматизацію у своєму проєкті та покращити досвід користувачів мобайл застосунків.
P.S. Буду вдячна, якщо поділитеся своїм досвідом роботи з мобайл автотестами в коментарях. Напишіть, які інструменти використовуєте? З якими складнощами стикалися та як їх вирішували? Які поради маєте щодо оптимізації роботи з ними?
Рецензент статті — Геннадій Міщевський.
16 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів