Azimjon Abdurasulov

QA 22: Mobil avtomatlashtirish — Maestro, Appium 3, XCUITest va Espresso

🧪 QA Engineer — Zero to Hero

QA 22: Mobil avtomatlashtirish — Maestro, Appium 3, XCUITest va Espresso

Holat: 2026-yil oktabr. Versiyalar: Maestro CLI 2.11.0, Appium 3.8.0 (XCUITest driver 12.16.0, UiAutomator2 driver 8.7.0), WebdriverIO 10.0.2, Xcode 26.5, Detox 20.51.4. WebdriverIO kodlari TypeScript kompilyatorida, XCUITest kodi iOS 26.5 SDK'da tekshirilgan. Maestro va Appium bu dars yozilgan muhitda ishga tushirilmagan — sintaksis rasmiy hujjatlardan olingan.

Bu darsdan keyin siz: mobil ilova uchun to'g'ri avtomatlashtirish vositasini tanlaysiz va birinchi barqaror testlaringizni yozasiz.

  • 2026-yilgi mobil avtomatlashtirish vositalari va ularning farqi;
  • ilovani testga tayyorlash: identifikatorlar, test rejimi, deep link;
  • Maestro bilan YAML flow'lar va CI;
  • Appium 3 arxitekturasi, WebdriverIO 10 bilan TypeScript testlar, locator va gesture'lar;
  • XCUITest (Swift) va Espresso/Compose (Kotlin) native testlar;
  • beqaror (flaky) mobil testlarning sabablari va davosi.

Mundarija

  1. Mobil avtomatlashtirish xaritasi (2026)
  2. Testability: ilovani testga tayyorlash
  3. Maestro: YAML bilan tez start
  4. Appium 3: arxitektura va o'rnatish
  5. WebdriverIO 10 + Appium: TypeScript'da birinchi test
  6. Locator strategiyasi va gesture'lar
  7. XCUITest: iOS native UI testlar
  8. Espresso va Compose testing: Android native
  9. Flaky mobil testlar: sabablar va davolash
  10. CI va qurilma fermalari
  11. Qaysi vositani tanlash kerak?
  12. Amaliy mashqlar
  13. O'zingizni tekshiring
  14. Xulosa
  15. Manbalar

1. Mobil avtomatlashtirish xaritasi (2026) {#xarita}

Mobil UI testlar web testlarga qaraganda sekinroq va qimmatroq. Shuning uchun test piramidasi (QA 4) bu yerda ayniqsa muhim:

  • pastda — ko'p unit testlar (Swift Testing/XCTest, JUnit);
  • o'rtada — komponent va API testlari (QA 20);
  • tepada — kam sonli, lekin muhim E2E ssenariylar: kirish, katalog, savatcha, buyurtma.
VositaKim yaratganTilPlatformaTuriQachon qulay
XCUITestAppleSwiftiOS, iPadOS, macOS, tvOSNative, alohida jarayondaiOS jamoasi o'z ilovasini testlaydi
EspressoGoogleKotlin/JavaAndroid (View)Native, ilova ichida, UI thread bilan sinxronAndroid View ilovalar
Compose UI testGoogleKotlinAndroid (Jetpack Compose)NativeCompose ekranlar
Appium 3Open source (OpenJS)JS/TS, Java, Python, C# va boshqalariOS, Android va boshqalarBlack-box, W3C WebDriverIkki platforma, bitta test kodi, device farm'lar
Maestromobile.dev (open source CLI)YAML (+ JavaScript)Android, iOS, Web; RN va FlutterBlack-box, accessibility daraxti orqaliTez start, QA jamoasi kod yozmasdan
DetoxWixJavaScriptReact NativeGray-boxReact Native ilovalar
Patrol / integration_testLeanCode / Flutter jamoasiDartFlutterNative + FlutterFlutter ilovalar (Patrol tizim dialoglari bilan ham ishlaydi)

🧠 Esda tuting: "Eng yaxshi vosita" yo'q — jamoaga mos vosita bor. Native dasturchilar o'zi test yozsa — XCUITest/Espresso. Alohida QA jamoasi ikki platformani tez qamrab olmoqchi bo'lsa — Maestro yoki Appium.


2. Testability: ilovani testga tayyorlash {#testability}

Avtomatlashtirish kod yozishdan emas, ilovani testga qulay qilishdan boshlanadi. Bu QA va dasturchilarning birgalikdagi ishi.

2.1 Barqaror identifikatorlar

Matn bo'yicha qidirish ("Kirish") til o'zgarganda buziladi. DemoBozor uch tilda ishlaydi — o'zbek, rus, ingliz. Shuning uchun har bir muhim element foydalanuvchiga ko'rinmaydigan identifikatorga ega bo'lishi kerak:

// SwiftUI
TextField("Telefon raqam", text: $phone)
    .accessibilityIdentifier("login.phone")

// UIKit
requestCodeButton.accessibilityIdentifier = "login.requestCode"
// Jetpack Compose
OutlinedTextField(
    value = phone,
    onValueChange = { phone = it },
    modifier = Modifier.testTag("login.phone")
)

// Ildiz composable'da bir marta: testTag'lar UiAutomator/Appium/Maestro uchun resource-id bo'lib ko'rinadi
Scaffold(modifier = Modifier.semantics { testTagsAsResourceId = true }) { /* ... */ }

Android View'da esa android:id="@+id/phoneInput" — u avtomatlashtirish vositalarida to'liq uz.demobozor.app:id/phoneInput ko'rinishidagi resource-id bo'lib chiqadi. Compose testTag esa (testTagsAsResourceId yoqilganda) aynan o'zi — login.phone — resource-id bo'ladi.

Nomlash qoidasi (jamoada kelishib oling): ekran.element — login.phone, login.requestCode, otp.input, home.title, cart.badge, product.addToCart. Bir xil mantiqiy elementga iOS va Android'da bir xil nom bersangiz, Maestro (id: selektori) va Appium testlari ikki platformada bir xil ishlaydi.

⚠️ Diqqat: accessibilityIdentifier va contentDescription/accessibilityLabel — boshqa-boshqa narsalar. Label'ni ekran o'quvchi (VoiceOver/TalkBack) foydalanuvchiga o'qib beradi. Unga login_btn_2 kabi texnik nom yozish accessibility'ni buzadi (QA 25). Test uchun identifier yoki testTag ishlating.

2.2 Test rejimi (launch arguments)

Test ilovani boshqacha ishga tushirishi kerak: staging API, animatsiyalarsiz, onboarding'siz, qat'iy sana bilan. Buning uchun launch argument'lar ishlatiladi:

// iOS ilova kodi (AppDelegate yoki App init)
if ProcessInfo.processInfo.arguments.contains("-uiTesting") {
    UIView.setAnimationsEnabled(false)
    // staging API, onboarding'ni o'tkazib yuborish, test ma'lumotlari...
}

Android'da argument'lar intent extras sifatida keladi (Maestro hujjatidagi usul):

val isUiTesting = intent.extras?.getBoolean("uiTesting") ?: false

2.3 Deep link — uzun yo'lni qisqartirish

"Buyurtma tafsilotlari" ekranini testlash uchun har safar login → katalog → buyurtmalar → #1043 yo'lini bosib o'tish shart emas: demobozor://orders/1043 deep link bilan to'g'ridan-to'g'ri ochiladi. Login holatini esa API orqali tayyorlash mumkin (QA 20).

2.4 Test ma'lumotlari va SMS kod

SMS kodni avtomatik testda haqiqiy telefondan o'qish — beqarorlik manbai. Odatiy yechim: staging'da maxsus test raqamlari uchun oldindan ma'lum kod.

⚠️ Diqqat: Bunday "test kodi" faqat staging'da va faqat test raqamlari uchun ishlashi kerak. Prod'da bunday "orqa eshik" qolsa, bu Critical xavfsizlik zaifligi. QA buni release build va prod API'da alohida tekshiradi (QA 24).

2.5 Animatsiyalarni o'chirish (Android)

adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0

3. Maestro: YAML bilan tez start {#maestro}

Maestro — ochiq kodli UI avtomatlashtirish freymvorki. Testlar YAML'da yoziladi va Flow deb ataladi. Maestro qurilmani "tashqaridan" boshqaradi: accessibility daraxtini o'qiydi va teginish, surish, matn kiritish buyruqlarini yuboradi. Android va iOS'da u qurilmaga kichik "companion driver" ilovani o'rnatib, u bilan ishlaydi. Shuning uchun tizim dialoglari va bildirishnomalar bilan ham ishlay oladi.

Kuchli tomoni — o'rnatilgan sabr: assertVisible element darhol ko'rinmasa ham 7 soniyagacha avtomatik kutadi va qayta uriniladi. sleep yozish deyarli kerak bo'lmaydi.

O'rnatish (macOS)

Talab: Java 17+ (java -version), Xcode va Command Line Tools.

curl -fsSL "https://get.maestro.mobile.dev" | bash
maestro --help

Homebrew orqali ham bo'ladi (mobile-dev-inc/tap). Vizual muharrir — Maestro Studio desktop ilovasi: qurilma ekranini ko'rsatadi, element ustiga bossangiz, mos buyruqni taklif qiladi.

Loyiha tuzilmasi

flows/
├── config.yaml              # qaysi flow'lar ishlashini belgilaydi
├── login_smoke.yaml
├── cart_add_product.yaml
├── offline_catalog.yaml
└── subflows/
    └── login.yaml           # qayta ishlatiladigan qadam (runFlow bilan chaqiriladi)

Subflow: kirish

# subflows/login.yaml — boshqa flow'lar qayta ishlatadigan kirish qadami
appId: uz.demobozor.app
env:
  PHONE: ${PHONE || "901234567"}
---
- tapOn:
    id: "login.phone"
- inputText: ${PHONE}
- tapOn:
    id: "login.requestCode"
- assertVisible:
    id: "otp.input"
- inputText: ${OTP}
- assertVisible:
    id: "home.title"

${PHONE || "901234567"} — parametr berilmasa, standart test raqami ishlatiladi.

Smoke flow

# flows/login_smoke.yaml — DemoBozor: SMS kod orqali kirish (smoke)
appId: uz.demobozor.app
tags:
  - smoke
  - auth
---
- launchApp:
    clearState: true
    permissions:
      notifications: deny
    arguments:
      uiTesting: true
- runFlow:
    file: subflows/login.yaml
    label: Test raqami bilan kirish
- takeScreenshot: login_success

clearState: true — toza o'rnatish holatidan boshlash. permissions — ruxsat dialogini oldindan hal qilish (bu yerda bildirishnoma rad etiladi). arguments esa ilovaga test rejimi bayrog'ini uzatadi (2.2).

Savatcha flow'i: scroll va tekshiruvlar

# flows/cart_add_product.yaml — katalogdan mahsulotni savatchaga qo'shish
appId: uz.demobozor.app
tags:
  - regression
  - cart
---
- launchApp:
    arguments:
      uiTesting: true
- runFlow: subflows/login.yaml
- tapOn:
    id: "tab.catalog"
- scrollUntilVisible:
    element:
      id: "product.gilam-2x3"
    direction: DOWN
    timeout: 20000
- tapOn:
    id: "product.gilam-2x3"
- tapOn:
    id: "product.addToCart"
- assertVisible:
    id: "cart.badge"
    text: "1"
- tapOn:
    id: "tab.cart"
- assertVisible: "Gilam \\(2x3 m\\)"
- assertVisible:
    id: "cart.deliveryFee"

Ikki nozik joy:

  • Maestro'da text va id selektorlari sukut bo'yicha regex. Shuning uchun qavslarni ekranlash kerak: Gilam \(2x3 m\) (YAML qo'sh tirnoq ichida \\( deb yoziladi).
  • Bir nechta atributni birga yozish (id + text) — VA sharti: ikkalasi ham mos bo'lishi kerak.

Offline holat (faqat Android)

# flows/offline_catalog.yaml — internet yo'qligida katalog (faqat Android)
appId: uz.demobozor.app
tags:
  - android-only
---
- launchApp
- setAirplaneMode: enabled
- tapOn:
    id: "tab.catalog"
- assertVisible:
    id: "error.offline"
- setAirplaneMode: disabled
- tapOn:
    id: "error.retry"
- assertVisible:
    id: "catalog.list"

Hujjatga ko'ra setAirplaneMode faqat Android'da ishlaydi. iOS simulatorda "parvoz rejimi" yo'q: buyruq xatosiz o'tadi, lekin hech narsa qilmaydi. iOS uchun offline'ni Proxyman yoki Network Link Conditioner bilan simulyatsiya qiling (QA 15).

Konfiguratsiya va ishga tushirish

# flows/config.yaml — qaysi flow'lar ishga tushishini belgilaydi
flows:
  - "*"
  - "!subflows/**"

Sukut bo'yicha Maestro faqat papkaning yuqori darajasidagi flow'larni ishga tushiradi, shuning uchun subflows/ o'zi ham ishlamaydi. ! bilan boshlanuvchi qator esa niyatni aniq ko'rsatadi.

# Faqat smoke testlar, JUnit hisobot bilan
maestro test flows/ --include-tags=smoke --format=JUNIT --output=maestro-report.xml \
  -e PHONE=901234567 -e OTP=000000

# Ekrandagi elementlar daraxtini terminalda ko'rish
maestro hierarchy

# Flow'ni MP4 videoga yozish (--local: video kompyuteringizda render qilinadi)
maestro record --local flows/login_smoke.yaml login_smoke.mp4

Boshqa foydali buyruqlar: openLink (deep link), setLocation, setPermissions, swipe, back, hideKeyboard, extendedWaitUntil (7 soniyadan uzoq kutish), retry, repeat, runScript (JavaScript) va boshqalar.

⚠️ Diqqat: Maestro hujjatiga ko'ra inputText Android'da Unicode belgilarni qo'llab-quvvatlamaydi. Kirill (Ташкент), oʻ/gʻ va emoji'larni Android'da inputText bilan kiritib bo'lmaydi. Bunday testlar uchun ma'lumotni API orqali tayyorlang, clipboard'dan foydalaning (setClipboard + pasteText) yoki boshqa vosita tanlang. Bu cheklovni test rejasiga albatta yozing.

💡 Maslahat: maestro mcp — Maestro'ning MCP serveri. Claude Code kabi AI yordamchilar u orqali flow yozib, ishga tushirib, xatosini tahlil qila oladi (QA 26).


4. Appium 3: arxitektura va o'rnatish {#appium}

Appium — mobil ilovalar uchun ochiq kodli avtomatlashtirish platformasi. U W3C WebDriver protokolida ishlaydi, ya'ni Selenium bilan bir xil g'oya: test kodi HTTP so'rov yuboradi, server esa uni qurilmadagi amalga aylantiradi.

Appium 3 arxitekturasi: test kodi, Appium server, driver'lar va qurilmalar

QismVazifasi
KlientTest kodi: WebdriverIO (JS/TS), Java, Python, C# va boshqalar
Appium serverNode.js dasturi; sukut bo'yicha http://127.0.0.1:4723 da so'rov kutadi
DriverPlatformaga xos ko'prik: iOS uchun XCUITest driver, Android uchun UiAutomator2 driver
Qurilmadagi agentiOS'da WebDriverAgent (XCTest asosidagi runner), Android'da UiAutomator2 server APK'si
Plugin'larServer imkoniyatlarini kengaytiradi, masalan, inspector

Appium 3 da nima o'zgardi?

Appium 3 2025-yil avgustda chiqdi va "tozalash" relizi bo'ldi:

  • Node.js ≥ 20.19 (^20.19.0 || ^22.12.0 || >=24.0.0) va npm ≥ 10 talab qilinadi;
  • 100 ga yaqin eski endpoint olib tashlandi: masalan, /touch/* o'rniga W3C Actions yoki mobile: buyruqlari ishlatiladi;
  • --allow-insecure funksiyalariga driver prefiksi majburiy: appium --allow-insecure=uiautomator2:adb_shell yoki barcha driverlar uchun *:adb_shell;
  • GET /sessions o'rniga GET /appium/sessions (alohida flag bilan);
  • ilova arxivini endi server emas, driver ochadi — driverlarni yangilab turing;
  • ichki Express 5 ga o'tkazildi (oddiy foydalanuvchiga ta'sir qilmaydi).

O'rnatish (o'z kompyuteringizda)

npm install -g appium                 # Appium 3 server
appium driver install xcuitest        # iOS driveri (Xcode kerak)
appium driver install uiautomator2    # Android driveri (Android SDK + JDK kerak)
appium driver doctor uiautomator2     # muhitni tekshirish: ANDROID_HOME, JAVA_HOME...
appium driver list --installed

appium plugin install inspector       # Appium Inspector plugin'i
appium --use-plugins=inspector        # Inspector: http://127.0.0.1:4723/inspector

Appium Inspector — ekrandagi elementlar daraxtini, ularning identifikatorlari va xususiyatlarini ko'rsatadigan vosita. U ikki ko'rinishda bor: desktop ilova (GitHub Releases) va Appium server plugin'i. Eskirgan "Appium Desktop" endi yo'q. Avvalgi inspector.appiumpro.com veb-versiyasi esa Appium jamoasiga aloqador emas.

Capabilities

Sessiya ochishda klient capabilities yuboradi. Appium'ga xos kalitlar appium: prefiksi bilan yoziladi:

{
  "platformName": "Android",
  "appium:automationName": "UiAutomator2",
  "appium:deviceName": "Pixel_7_API_34",
  "appium:app": "/Users/qa/apps/demobozor-staging.apk",
  "appium:noReset": false
}
{
  "platformName": "iOS",
  "appium:automationName": "XCUITest",
  "appium:deviceName": "iPhone 17",
  "appium:platformVersion": "26.5",
  "appium:app": "/Users/qa/apps/DemoBozor.app",
  "appium:noReset": false
}

appium:noReset: false — har sessiyada ilova holatini tozalash. true bo'lsa, oldingi test qoldirgan ma'lumot keyingisiga ta'sir qiladi (flaky test manbai).


5. WebdriverIO 10 + Appium: TypeScript'da birinchi test {#webdriverio}

WebdriverIO — Node.js uchun WebDriver klienti va test runner'i. WebdriverIO 10 Appium 3 ni talab qiladi: uning Appium service'i Appium 1.x/2.x ni ishga tushirmaydi.

npm init wdio@latest .                                  # konfiguratsiya ustasi
npm install @wdio/appium-service appium@^3 --save-dev   # Appium'ni loyiha ichida saqlash

Konfiguratsiya

// wdio.conf.ts — Android emulatorda DemoBozor testlari (Appium 3 + UiAutomator2)
export const config: WebdriverIO.Config = {
  runner: "local",
  specs: ["./test/specs/**/*.e2e.ts"],
  maxInstances: 1,
  port: 4723, // Appium'ning standart porti
  services: ["appium"],
  framework: "mocha",
  reporters: ["spec"],
  mochaOpts: { timeout: 120_000 },
  capabilities: [
    {
      platformName: "Android",
      "appium:automationName": "UiAutomator2",
      "appium:deviceName": "Pixel_7_API_34",
      "appium:app": "./apps/demobozor-staging.apk",
      "appium:appWaitActivity": "*",
      "appium:noReset": false,
    },
  ],
};

services: ['appium'] — testlar boshlanganda Appium serverini avtomatik ishga tushiradi va oxirida to'xtatadi.

Screen Object (mobil Page Object)

Web'dagi Page Object Model (QA 19) mobil'da Screen Object deb ham ataladi:

// test/helpers/byTestId.ts — bitta identifier, ikki platforma
import { $, driver } from "@wdio/globals";

// iOS: accessibilityIdentifier -> ~login.phone
// Android: Compose testTag (testTagsAsResourceId bilan) -> resource-id "login.phone"
export function byTestId(id: string) {
  return driver.isAndroid
    ? $(`android=new UiSelector().resourceId("${id}")`)
    : $(`~${id}`);
}
// test/screens/LoginScreen.ts — mobil Page Object (Screen Object)
import { byTestId } from "../helpers/byTestId.js";

class LoginScreen {
  get phoneInput() {
    return byTestId("login.phone");
  }
  get requestCodeButton() {
    return byTestId("login.requestCode");
  }
  get otpInput() {
    return byTestId("otp.input");
  }
  get errorText() {
    return byTestId("login.error");
  }

  async requestCode(phone: string): Promise<void> {
    await this.phoneInput.setValue(phone);
    await this.requestCodeButton.click();
  }

  async enterOtp(code: string): Promise<void> {
    await this.otpInput.waitForDisplayed({ timeout: 10_000 });
    await this.otpInput.setValue(code);
  }
}

export default new LoginScreen();
// test/screens/HomeScreen.ts
import { byTestId } from "../helpers/byTestId.js";

class HomeScreen {
  get title() {
    return byTestId("home.title");
  }
  get catalogTab() {
    return byTestId("tab.catalog");
  }
}

export default new HomeScreen();

Test

// test/specs/login.e2e.ts — SMS kod bilan kirish (staging test raqami)
import { expect } from "@wdio/globals";
import LoginScreen from "../screens/LoginScreen.js";
import HomeScreen from "../screens/HomeScreen.js";

const TEST_PHONE = process.env.TEST_PHONE ?? "901234567";
const TEST_OTP = process.env.TEST_OTP ?? "";

describe("DemoBozor: kirish", () => {
  it("to'g'ri SMS kod bilan bosh sahifa ochiladi", async () => {
    await LoginScreen.requestCode(TEST_PHONE);
    await LoginScreen.enterOtp(TEST_OTP);
    await expect(HomeScreen.title).toBeDisplayed();
  });

  it("noto'g'ri kodda xato matni chiqadi", async () => {
    await LoginScreen.requestCode(TEST_PHONE);
    await LoginScreen.enterOtp("000001");
    await expect(LoginScreen.errorText).toBeDisplayed();
  });
});

Ishga tushirish:

TEST_OTP=000000 npx wdio run ./wdio.conf.ts

byTestId yordamchisi bitta nom bilan ikki platformada qidiradi. iOS'da ~login.phone — accessibility id selektori, u accessibilityIdentifier bo'yicha qidiradi. Android'da esa resource-id bo'yicha qidiriladi: Compose testTag (testTagsAsResourceId bilan). Nega Android'da ham ~ ishlatmaymiz? UiAutomator2'da ~ content-desc ni qidiradi, u esa ekran o'quvchi foydalanuvchiga o'qib beradigan matn. Unga texnik nom yozish accessibility'ni buzadi (2.1).

expect(...).toBeDisplayed() — WebdriverIO'ning avtomatik kutadigan tekshiruvi: element ko'ringuncha qayta uriniladi.


6. Locator strategiyasi va gesture'lar {#locators-gestures}

Locator ustuvorligi (mobil)

O'rinStrategiyaWebdriverIO misoliIzoh
1Accessibility ID (iOS)$('~cart.badge')iOS'da eng barqaror va tez
1Resource ID (Android)$(android=new UiSelector().resourceId("cart.badge")) yoki $('id=uz.demobozor.app:id/cartBadge')Compose testTag / View android:id
2iOS predicate string$(-ios predicate string:type == 'XCUIElementTypeButton' AND name == 'tab.cart')Murakkab shartlar, tez
2iOS class chain$('-ios class chain:**/XCUIElementTypeCell[1]')Ierarxiya bo'yicha
2Android UiSelector$(android=new UiSelector().text("Savatcha"))Matn, class, tartib
3XPath$('//android.widget.Button[@text="Savatcha"]')Oxirgi chora: sekin va mo'rt

⚠️ Diqqat: XPath mobil'da ayniqsa sekin. Appium har safar butun elementlar daraxtini XML'ga aylantiradi. Layout ozgina o'zgarsa ham XPath buziladi. Agar element'ga XPath'siz yetib bo'lmasa, bu — dasturchidan identifier so'rash uchun signal.

Gesture'lar

WebdriverIO 10'da native ilovalar uchun yuqori darajali buyruqlar bor (ular ichkarida W3C Actions va driver buyruqlaridan foydalanadi):

// gestures.e2e.ts — scroll, swipe va tap misollari
import { $, browser, driver, expect } from '@wdio/globals';
import { byTestId } from '../helpers/byTestId.js';

describe('DemoBozor: katalog gesture\'lari', () => {
  it('mahsulotgacha scroll qilib, savatchaga qo\'shadi', async () => {
    await byTestId('tab.catalog').tap();

    // direction: 'up' — barmoq yuqoriga suriladi, ro'yxat pastga ko'chadi (standart qiymat)
    const product = byTestId('product.gilam-2x3');
    await product.scrollIntoView({ direction: 'up', maxScrolls: 10 });
    await product.tap();

    await byTestId('product.addToCart').tap();
    await expect(byTestId('cart.badge')).toHaveText('1');
  });

  it('banner karuselini chapga suradi', async () => {
    await browser.swipe({ direction: 'left', scrollableElement: byTestId('home.banners') });
  });

  it('platformaga xos selektorlar', async () => {
    // Android: UiAutomator selektori
    const uiSelector = 'new UiSelector().text("Savatcha").className("android.widget.TextView")';
    const cartTabAndroid = $(`android=${uiSelector}`);

    // iOS: predicate string va class chain
    const cartTabIos = $(`-ios predicate string:type == 'XCUIElementTypeButton' AND name == 'tab.cart'`);
    const firstCell = $('-ios class chain:**/XCUIElementTypeCell[1]');

    const isAndroid = driver.isAndroid;
    await (isAndroid ? cartTabAndroid : cartTabIos).tap();
    if (!isAndroid) {
      await expect(firstCell).toBeDisplayed();
    }
  });

  it('W3C Actions bilan pastga tortib yangilash (pull-to-refresh)', async () => {
    const { width, height } = await driver.getWindowSize();
    await browser
      .action('pointer', { parameters: { pointerType: 'touch' } })
      .move({ x: Math.round(width / 2), y: Math.round(height * 0.3) })
      .down()
      .pause(100)
      .move({ duration: 400, x: Math.round(width / 2), y: Math.round(height * 0.8) })
      .up()
      .perform();
  });
});
BuyruqNima qiladi
$(sel).tap()Elementni bosadi; kerak bo'lsa avval unga scroll qiladi (click dan ishonchliroq)
$(sel).scrollIntoView({ maxScrolls })Element ko'ringuncha suradi (standart: 10 marta)
browser.swipe({ direction, scrollableElement })Ekran yoki element ichida surish
browser.action('pointer', ...)Past darajadagi W3C Actions: istalgan harakat
driver.execute('mobile: scrollGesture', {...}) / 'mobile: scroll'Android/iOS driverining o'z buyruqlari

💡 Maslahat: Koordinatalar bo'yicha bosishdan (x: 120, y: 640) qoching — boshqa ekran o'lchamida test buziladi. Yuqoridagi pull-to-refresh misolida ham koordinatalar ekran o'lchamiga nisbatan (height * 0.3) hisoblangan.


7. XCUITest: iOS native UI testlar {#xcuitest}

XCUITest — Apple'ning UI testlash API'si. Apple hujjatlarida u endi XCUIAutomation freymvorki sifatida tasvirlanadi. Testlar alohida jarayonda ishlaydi va ilovani accessibility orqali boshqaradi. U Xcode bilan birga keladi va qo'shimcha o'rnatish talab qilmaydi.

Muhim fakt: unit testlar uchun yangi Swift Testing freymvorki tavsiya qilinadi, lekin Apple hujjati aniq aytadi — UI testlar va performance testlar uchun XCTest'dan foydalanishda davom eting (testing darsi).

import XCTest

final class DemoBozorLoginUITests: XCTestCase {
    private var app: XCUIApplication!

    override func setUpWithError() throws {
        continueAfterFailure = false
        app = XCUIApplication()
        // Test rejimi: staging API, animatsiyalarsiz, onboarding'siz
        app.launchArguments += ["-uiTesting", "-disableAnimations"]
        app.launchEnvironment["API_BASE_URL"] = "https://staging-api.demobozor.example"
        app.launch()
    }

    func testLoginWithValidOtpOpensHome() throws {
        let phoneField = app.textFields["login.phone"]
        XCTAssertTrue(phoneField.waitForExistence(timeout: 5))
        phoneField.tap()
        phoneField.typeText("901234567")

        app.buttons["login.requestCode"].tap()

        let otpField = app.textFields["otp.input"]
        XCTAssertTrue(otpField.waitForExistence(timeout: 10))
        otpField.typeText(ProcessInfo.processInfo.environment["TEST_OTP"] ?? "")

        XCTAssertTrue(app.staticTexts["home.title"].waitForExistence(timeout: 10))
    }

    func testNotificationPermissionAlertCanBeDismissed() throws {
        // Tizim oynalari ilovaga emas, SpringBoard'ga tegishli
        let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
        let dontAllow = springboard.buttons["Don’t Allow"]
        if dontAllow.waitForExistence(timeout: 3) {
            dontAllow.tap()
        }
        XCTAssertTrue(app.textFields["login.phone"].waitForExistence(timeout: 5))
    }
}

Kodga izohlar:

  • app.textFields["login.phone"] — identifier bo'yicha qidirish (2.1).
  • waitForExistence(timeout:) — kutish; sleep() emas.
  • Ruxsat dialoglari SpringBoard jarayoniga tegishli. Tugma matni ("Don’t Allow" — tipografik apostrof bilan) qurilma tiliga bog'liq, shuning uchun UI testlarni simulyator tili belgilangan holda ishga tushiring.
  • continueAfterFailure = false — birinchi xatodan keyin test to'xtaydi va keyingi qadamlarda chalg'ituvchi xatolar chiqmaydi.

Yozib olish (recording): Xcode'da UI test metodi ichiga kursorni qo'yib, muharrir chetidagi qizil yozish tugmasini bosasiz. Ilova bilan ishlaganingizda Xcode element so'rovlarini kodga o'zi qo'shadi. Natijani doim tozalang: yozilgan kodda ko'pincha mo'rt so'rovlar bo'ladi.

Terminaldan ishga tushirish

xcodebuild test \
  -scheme DemoBozor \
  -destination 'platform=iOS Simulator,name=iPhone 17' \
  -only-testing:DemoBozorUITests \
  -resultBundlePath build/DemoBozorUITests.xcresult

# Flaky testni ovlash: muvaffaqiyatsiz bo'lguncha 20 martagacha takrorlash
xcodebuild test -scheme DemoBozor \
  -destination 'platform=iOS Simulator,name=iPhone 17' \
  -only-testing:DemoBozorUITests/DemoBozorLoginUITests/testLoginWithValidOtpOpensHome \
  -test-iterations 20 -run-tests-until-failure

-retry-tests-on-failure bayrog'i ham bor. Lekin uni faqat flaky testni vaqtincha yumshatish uchun ishlating — sababini topib tuzatish kerak (QA 21).


8. Espresso va Compose testing: Android native {#espresso-compose}

Espresso (View asosidagi ekranlar)

Espresso ilova ichida ishlaydi va UI thread bilan sinxronlashadi: ekran "tinchlanmaguncha" keyingi qadamga o'tmaydi.

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class LoginEspressoTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    private val testOtp: String
        get() = InstrumentationRegistry.getArguments().getString("testOtp").orEmpty()

    @Test
    fun validOtpOpensHome() {
        onView(withId(R.id.phoneInput)).perform(typeText("901234567"), closeSoftKeyboard())
        onView(withId(R.id.requestCodeButton)).perform(click())
        onView(withId(R.id.otpInput)).perform(typeText(testOtp), closeSoftKeyboard())
        onView(withId(R.id.homeTitle)).check(matches(isDisplayed()))
    }
}

⚠️ Diqqat: Espresso UI thread'ni kutadi, lekin tarmoq so'rovlarini (coroutine'lar, OkHttp) avtomatik kutmaydi. Bunday holatlar uchun IdlingResource yoziladi yoki testda mock server ishlatiladi. Thread.sleep() — yechim emas, flaky testga yo'l.

Compose UI test

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import org.junit.Rule
import org.junit.Test

class LoginComposeTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Test
    fun validOtpOpensHome() {
        composeTestRule.onNodeWithTag("login.phone").performTextInput("901234567")
        composeTestRule.onNodeWithText("Kodni olish").performClick()

        // Tarmoq javobini kutish: otp maydoni paydo bo'lguncha (10 soniyagacha)
        composeTestRule.waitUntil(timeoutMillis = 10_000) {
            composeTestRule.onAllNodesWithTag("otp.input").fetchSemanticsNodes().isNotEmpty()
        }
        composeTestRule.onNodeWithTag("otp.input").performTextInput("000000")
        composeTestRule.onNodeWithTag("home.title").assertIsDisplayed()
    }
}

Ishga tushirish (ulangan qurilma yoki emulatorda):

./gradlew connectedDebugAndroidTest \
  -Pandroid.testInstrumentationRunnerArguments.testOtp=000000

onNodeWithText("Kodni olish") — matn bo'yicha qidirish. Bu test faqat o'zbek tilida o'tadi. Ko'p tilli ilovada onNodeWithTag afzal.


9. Flaky mobil testlar: sabablar va davolash {#flaky}

Flaky test — kod o'zgarmasa ham ba'zan o'tib, ba'zan yiqiladigan test. Mobil'da bunday testlar web'dagidan ko'proq uchraydi.

SababBelgisiDavosi
AnimatsiyalarElement "harakatda" — bosish o'tib ketadiTest rejimida animatsiyalarni o'chirish (adb shell settings ..., iOS'da -uiTesting)
Qat'iy sleepSekin qurilmada yiqiladi, tezida vaqt behuda ketadiwaitForExistence, waitForDisplayed, Maestro'ning avtomatik kutishi
TarmoqStaging sekin bo'lgan kuni testlar qizilMock server, API orqali tayyorlangan ma'lumot, timeout'lar
Holat oqib o'tishiOldingi test savatchada mahsulot qoldirganclearState, noReset: false, har test o'z ma'lumotini yaratadi
Umumiy test akkauntiParallel testlar bir-birining savatchasini o'zgartiradiHar parallel oqimga alohida akkaunt
Tizim dialoglari"Allow notifications?" kutilmaganda chiqadiRuxsatlarni oldindan berish (launchApp: permissions, simctl privacy, pm grant)
KlaviaturaKlaviatura tugmani yopib qo'yadihideKeyboard, closeSoftKeyboard()
KoordinatalarBoshqa ekranda boshqa joyga bosiladiIdentifier bo'yicha selektorlar
Til va vaqtIngliz tilli CI'da "Kirish" topilmaydiIdentifier'lar; simulyator tili va vaqt zonasini qat'iy belgilash

🎯 Intervyuda: "Mobil testlaringiz CI'da ba'zan yiqiladi. Nima qilasiz?" — Avval qayta urinish (retry) bilan "yashirmayman". Testni ko'p marta ishga tushirib (masalan, -test-iterations 20 -run-tests-until-failure) takrorlanish ehtimolini o'lchayman. Video va loglardan sababni topaman: kutish, holat, tarmoq yoki animatsiya. Sabab topilguncha testni karantinga olaman va ticket ochaman.


10. CI va qurilma fermalari {#ci}

iOS

iOS UI testlar faqat macOS runner'larda ishlaydi (GitHub Actions'ning macOS image'lari, Xcode Cloud, Bitrise). Asosiy buyruq — yuqoridagi xcodebuild test. Batafsil iOS CI — DevOps 1: iOS CI/CD darsida.

Android: emulator + Maestro (GitHub Actions)

Linux runner'da Android emulatorini reactivecircus/android-emulator-runner action'i (v2) ko'taradi. Hardware acceleration uchun KVM ruxsatlari yoqiladi:

name: android-e2e

on:
  pull_request:
  workflow_dispatch:

jobs:
  maestro-android:
    runs-on: ubuntu-latest
    timeout-minutes: 45
    steps:
      - uses: actions/checkout@v7

      - uses: actions/setup-java@v6
        with:
          distribution: temurin
          java-version: "17"

      - name: KVM ruxsatlarini yoqish
        run: |
          echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
          sudo udevadm control --reload-rules
          sudo udevadm trigger --name-match=kvm

      - name: Maestro CLI o'rnatish
        run: |
          curl -fsSL "https://get.maestro.mobile.dev" | bash
          echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"

      - name: Debug APK yig'ish
        run: ./gradlew assembleDebug

      - name: Emulatorda smoke testlar
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          target: google_apis
          arch: x86_64
          profile: pixel_7_pro
          script: |
            adb install -r app/build/outputs/apk/debug/app-debug.apk
            maestro test flows/ --include-tags=smoke --format=JUNIT --output=maestro-report.xml -e PHONE=901234567 -e OTP=${{ secrets.STAGING_TEST_OTP }}

      - name: Hisobotni saqlash
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: maestro-report
          path: maestro-report.xml

Bu YAML sintaksis jihatidan tekshirilgan. Action versiyalari 2026-yil oktabr holatiga mos: checkout@v7, setup-java@v6, upload-artifact@v7, android-emulator-runner@v2. CI'ning umumiy tamoyillari (sharding, hisobotlar, quality gate) — QA 21.

Qurilma fermalari

Ko'p real qurilmada parallel test uchun BrowserStack App Automate, Sauce Labs Real Device Cloud, TestMu AI (sobiq LambdaTest), AWS Device Farm yoki Firebase Test Lab ishlatiladi (QA 14). Appium testlari odatda faqat server manzili va capabilities'ni o'zgartirib, fermaga ko'chiriladi. Maestro'ning esa o'z bulut xizmati — Maestro Cloud (maestro cloud --app-file ... --flows ...) bor.


11. Qaysi vositani tanlash kerak? {#tanlov}

VaziyatTavsiyaNega
iOS jamoasi o'z ilovasiga test yozadiXCUITestXcode ichida, Swift, qo'shimcha infratuzilma yo'q
Android jamoasi, Compose ekranlarCompose UI test (+ Espresso)Tez, ilova ichida, sinxronizatsiya
Alohida QA jamoasi, iOS + Android, tez natija kerakMaestroYAML, o'rganish oson, tizim dialoglari bilan ishlaydi
Katta regression to'plami, ko'p qurilma, mavjud Selenium/JS tajribasiAppium 3 + WebdriverIO 10Bitta kod ikki platformaga, fermalar qo'llab-quvvatlaydi
React NativeDetox yoki MaestroGray-box sinxronizatsiya / tez start
FlutterPatrol, integration_test yoki MaestroFlutter widget'lari va native dialoglar
Ilova kodiga kirish yo'q (black-box)Maestro yoki AppiumFaqat tayyor build kerak

💡 Maslahat: Bitta ilovada ikki qatlam bo'lishi normal: dasturchilar komponent darajasida XCUITest/Compose testlarini yozadi, QA esa 15–30 ta asosiy E2E ssenariyni Maestro yoki Appium bilan qamrab oladi.


12. Amaliy mashqlar {#mashqlar}

Mashq 1: Vosita tanlash

Har bir vaziyat uchun vosita tanlang va bir jumla bilan asoslang:

  1. Startapda 1 ta QA bor, Flutter ilova, 2 haftada smoke testlar kerak.
  2. Bank: Android (Kotlin, Compose) va iOS (SwiftUI) native jamoalari, kuchli dasturchilar.
  3. Outsourcing kompaniyasi: 5 ta mijoz ilovasi, BrowserStack'da 30 ta qurilmada tungi regression.
  4. React Native e-commerce, testlar ko'pincha animatsiya va tarmoq tufayli yiqiladi.

<details> <summary>💡 Javobni ko'rish</summary>

  1. Maestro — YAML tez o'rganiladi, Flutter accessibility orqali qo'llab-quvvatlanadi, kod yozish shart emas.
  2. XCUITest + Compose UI test — native jamoalar o'z tilida, ilova kodiga yaqin test yozadi.
  3. Appium 3 + WebdriverIO — bitta kod bazasi, capabilities bilan fermadagi istalgan qurilma, standart W3C protokoli.
  4. Detox (gray-box sinxronizatsiya animatsiya va tarmoqni kutadi) yoki Maestro (avtomatik kutish); qo'shimcha ravishda test rejimida animatsiyalarni o'chirish.

</details>

Mashq 2: Maestro flow yozing

Ssenariy: qidiruvga "choynak" yozish (qidiruv 2–100 belgi), birinchi natijani ochish, savatchaga qo'shish, savatcha belgisida 1 borligini va yetkazib berish narxi ko'rinishini tekshirish. Identifikatorlar: tab.search, search.input, search.result.0, product.addToCart, cart.badge, tab.cart, cart.deliveryFee.

<details> <summary>💡 Javobni ko'rish</summary>

appId: uz.demobozor.app
tags:
  - regression
  - search
---
- launchApp:
    arguments:
      uiTesting: true
- runFlow: subflows/login.yaml
- tapOn:
    id: "tab.search"
- tapOn:
    id: "search.input"
- inputText: "choynak"
- tapOn:
    id: "search.result.0"
- tapOn:
    id: "product.addToCart"
- assertVisible:
    id: "cart.badge"
    text: "1"
- tapOn:
    id: "tab.cart"
- assertVisible:
    id: "cart.deliveryFee"

Qo'shimcha: choynak narxi 300 000 so'mdan kam bo'lsa, yetkazib berish 20 000 so'm bo'lishi kerak. Matnni tekshirishda probel va valyuta formatiga e'tibor bering: assertVisible: ".*20 000.*" (regex).

</details>

Mashq 3: XPath'dan qutulish

Test kodida shunday locator bor: //android.widget.LinearLayout[2]/android.widget.FrameLayout[1]/android.widget.Button[@text="Savatchaga qo'shish"]. Muammolari nimada va qanday tuzatiladi?

<details> <summary>💡 Javobni ko'rish</summary>

Muammolar:

  • layout'ga bog'langan (LinearLayout[2]): dizayn o'zgarsa buziladi;
  • matnga bog'langan: rus yoki ingliz tilida ishlamaydi;
  • XPath sekin: har safar butun daraxt XML'ga aylantiriladi.

Yechim: dasturchidan identifier so'rash (product.addToCart) va testda byTestId('product.addToCart') ishlatish (iOS'da accessibilityIdentifier, Android'da Compose testTag + testTagsAsResourceId). Identifier qo'shish kod-review'da QA talabiga aylanishi kerak.

</details>

Mashq 4: iOS capabilities

Appium 3 uchun iOS Simulator (iPhone 17, iOS 26.5) capabilities'ini yozing. Ilova: ~/apps/DemoBozor.app, har sessiyada toza holat.

<details> <summary>💡 Javobni ko'rish</summary>

{
  "platformName": "iOS",
  "appium:automationName": "XCUITest",
  "appium:deviceName": "iPhone 17",
  "appium:platformVersion": "26.5",
  "appium:app": "/Users/qa/apps/DemoBozor.app",
  "appium:noReset": false
}

E'tibor bering: platformName standart W3C kaliti, shuning uchun prefikssiz yoziladi. Qolgan Appium kalitlari appium: prefiksi bilan yoziladi.

</details>

Mashq 5: Flaky testni toping

Quyidagi testda kamida 4 ta flaky manbani toping:

it("savatchaga qo'shish", async () => {
  await browser.pause(5000);
  await $('//android.widget.Button[@text="Katalog"]').click();
  await browser
    .action("pointer")
    .move({ x: 540, y: 1650 })
    .down()
    .up()
    .perform();
  await expect($("~cart.badge")).toHaveText("1"); // umumiy qa@demobozor akkaunti
});

<details> <summary>💡 Javobni ko'rish</summary>

  1. browser.pause(5000) — qat'iy kutish: sekin qurilmada yetmaydi, tezida vaqt behuda ketadi. waitForDisplayed yoki avtomatik kutadigan expect ishlating.
  2. Matnli XPath (@text="Katalog") — til va layout'ga bog'liq; byTestId('tab.catalog') ishlating.
  3. Absolyut koordinata (540, 1650) — boshqa ekran o'lchamida boshqa joy bosiladi.
  4. Umumiy akkaunt — boshqa test yoki parallel oqim savatchaga mahsulot qo'shgan bo'lsa, 1 emas, 2 chiqadi. Har test o'z akkaunti va toza savatcha bilan ishlashi kerak.
  5. Qo'shimcha: test savatchani oldindan tozalamaydi (noReset: false yoki API orqali tozalash).

</details>

Mashq 6: Bildirishnoma ruxsati (XCUITest)

Ilova birinchi ochilishda bildirishnoma ruxsatini so'raydi va bu dialog login testlarini to'sib qo'yyapti. Ikki xil yechim taklif qiling.

<details> <summary>💡 Javobni ko'rish</summary>

  1. Testda dialogni yopish: XCUIApplication(bundleIdentifier: "com.apple.springboard") orqali tugmani topib bosish (yuqoridagi misol). Kamchiligi: tugma matni qurilma tiliga bog'liq.
  2. Dialogni chiqarmaslik: test rejimida (-uiTesting) ilova ruxsatni login'dan keyinroq so'raydi yoki umuman so'ramaydi. Ruxsat oqimining o'zi esa alohida, maxsus testda tekshiriladi.

Qo'shimcha: Maestro'da launchApp: permissions: notifications: deny bilan bu masala bitta qatorda hal bo'ladi.

</details>


13. O'zingizni tekshiring {#checklist}

  • [ ] XCUITest, Espresso/Compose, Appium, Maestro, Detox va Patrol farqini tushuntira olaman
  • [ ] Dasturchidan qanday identifier va test rejimi so'rashni bilaman
  • [ ] Maestro'da subflow, tag, env va scroll bilan flow yoza olaman
  • [ ] Appium 3 arxitekturasini (klient → server → driver → qurilma) chizib bera olaman
  • [ ] WebdriverIO 10 bilan Screen Object va accessibility id'dan foydalanib test yoza olaman
  • [ ] XPath nega oxirgi chora ekanini tushuntira olaman
  • [ ] XCUITest'da waitForExistence va launch argument'lardan foydalana olaman
  • [ ] Flaky mobil testning 5 ta sababini va davosini bilaman

Xulosa {#xulosa}

  • Mobil E2E testlar qimmat — ularni piramidaning tepasida, eng muhim ssenariylar uchun yozing.
  • Avtomatlashtirish testability'dan boshlanadi: barqaror identifier'lar (login.phone), test rejimi (-uiTesting), deep link'lar va staging test ma'lumotlari.
  • Maestro — YAML, avtomatik kutish, tez start. Android'da Unicode matn kiritish cheklovini yodda tuting.
  • Appium 3 — W3C WebDriver, XCUITest/UiAutomator2 driverlari. Node 20.19+ talab qilinadi va --allow-insecure uchun driver prefiksi majburiy. WebdriverIO 10 faqat Appium 3 bilan ishlaydi.
  • XCUITest va Espresso/Compose — native jamoalar uchun eng tez va barqaror yo'l. UI testlar uchun Apple XCTest'da qolishni tavsiya qiladi.
  • Flaky testlarning asosiy sabablari: qat'iy sleep, animatsiyalar, umumiy ma'lumot, tizim dialoglari, koordinatalar. Retry bilan yashirmang — sababini toping.
  • Keyingi dars: QA 23: Performance testing — Load, Stress, Spike, Soak va k6 bilan amaliyot

Manbalar {#manbalar}

  1. Maestro Docs — What is Maestro
  2. Maestro Docs — How Maestro works
  3. Maestro Docs — How to install Maestro CLI
  4. Maestro Docs — CLI commands and options
  5. Maestro Docs — inputText
  6. Maestro Docs — launchApp
  7. Maestro Docs — setAirplaneMode
  8. Maestro Docs — Test discovery and tags
  9. Appium — Install Appium
  10. Appium — Migrating from Appium 2 to Appium 3
  11. Appium — Capabilities
  12. Appium — UiAutomator2 driver quickstart
  13. Appium Inspector — Installation
  14. WebdriverIO — Appium Service
  15. WebdriverIO — Selectors
  16. WebdriverIO — swipe
  17. WebdriverIO — scrollIntoView
  18. Apple — XCTest
  19. Apple — XCUIAutomation
  20. Android Developers — Espresso basics
  21. Android Developers — Test your Compose layout
  22. Android Developers — Compose interoperability with UiAutomator
  23. GitHub — ReactiveCircus/android-emulator-runner

Markdown manba

iOS Lessons