Azimjon Abdurasulov

Bazel 6: Testlar — ios_unit_test, XCTest va Swift Testing, test cache, coverage va UI testlar

📦 Bazel iOS — Mukammal Qo‘llanma & Amaliyot

Bazel 6: Testlar — ios_unit_test, XCTest va Swift Testing, test cache, coverage va UI testlar

Holat: 2026-yil oktabr. Bu darsda Bazel bilan iOS testlarini yozamiz va ishga tushiramiz: ios_unit_test, XCTest va Swift Testing, test natijasi keshi, coverage va UI testlar. Kod misollari Bazel 9.3.0 (Bazelisk 1.29.0), rules_apple 5.3.0, rules_swift 4.2.1 va Xcode 27.1 bilan Apple M4 Pro'da (12 yadro, 24 GB) tekshirilgan.

Bu darsdan keyin siz: iOS kutubxona va ilovasi uchun to'liq test to'plamini Bazel'da qura olasiz — logic testlardan tortib UI testlargacha — va Bazel'ning test keshidan foydalanib, o'zgarmagan testlarni qayta ishlatmay, sekundlarni tejaysiz.

  • swift_library + ios_unit_test bilan test target'ini to'g'ri tuzish (testonly qoidasi bilan)
  • XCTest va Swift Testing'ni bitta target'da yozish va ishga tushirish
  • bazel test, --test_output, --test_filter — kundalik buyruqlar
  • Test natijasi keshi: (cached) PASSED — qachon ishlaydi, qachon buziladi
  • size, timeout, flaky, --runs_per_test, shard_count — mo'rt (flaky) testlarni boshqarish
  • Logic test, hosted test (test_host) va UI test (ios_ui_test) — farqi, tezligi, qachon qaysi biri
  • bazel coverage bilan kod qamrovini (code coverage) o'lchash

Mundarija

  1. Nega testlar uchun alohida dars?
  2. Test anatomiyasi: uch rule
  3. Birinchi test: XCTest bilan
  4. Swift Testing: zamonaviy sintaksis
  5. Testlarni ishga tushirish va filtrlash
  6. Test natijasi keshi — Bazel'ning super-kuchi
  7. size, timeout va flaky testlar
  8. Simulyatorni tanlash va Xcode 27 tuzog'i
  9. Logic test va hosted test (test_host)
  10. UI testlar va bir timeout hikoyasi
  11. Coverage — kod qamrovi
  12. ios_build_test — "shunchaki quriladimi?"
  13. Katta jamoalarda test strategiyasi
  14. Amaliy mashqlar
  15. O'zingizni tekshiring
  16. Xulosa
  17. Manbalar

1. Nega testlar uchun alohida dars? {#nega-test-dars}

5-darsda biz Bazel bilan birinchi iOS ilovamizni qurdik: swift_library kodni kompilyatsiya qiladi, ios_application uni .app/.ipa'ga yig'adi, bazel run esa simulyatorda ishga tushiradi. Kod yozdik — endi uning to'g'ri ishlashini tekshirishimiz kerak.

Xcode'da testni bilasiz: Cmd+U bosasiz, Xcode butun test schema'sini qayta quradi va hammasini ishga tushiradi. Loyiha o'sgani sari bu sekinlashadi. Siz bitta modulning bitta funksiyasini o'zgartirasiz — Xcode esa hech aloqasi yo'q 200 ta boshqa test faylini ham qayta kompilyatsiya qilib, hammasini qayta ishga tushirishi mumkin. CI'da bu yanada og'ir: har PR'da to'liq test to'plami noldan quriladi.

Bazel bu yerda ikki narsani boshqacha qiladi — va ikkalasi ham shu darsning markazida:

  1. **Test — bu oddiy build target emas, balki alohida rule.** ios_unit_test nafaqat test .xctest bundle'ini quradi, balki uni simulyatorda ishga tushiradi va natijani (passed/failed) qaytaradi. Bu natija ham, boshqa har qanday action'ning chiqishi kabi, keshlanadi.
  2. O'zgarmagan test qayta ishlamaydi. Agar test binary va uning barcha kirishlari (inputlar) o'zgarmagan bo'lsa, Bazel testni simulyatorda qaytadan ishga tushirmaydi — oldingi natijani keshdan oladi va (cached) PASSED deb yozadi. Bu bir zumda bo'ladi.

🚀 Motivatsiya: Keyinroq o'z ko'zingiz bilan ko'rasiz: bizning test to'plamimiz birinchi marta 1.8 soniyada o'tdi; hech narsa o'zgarmaganda ikkinchi marta ishga tushirsak, Bazel uni umuman ishga tushirmay, 0.19 soniyada (cached) PASSED dedi. Minglab testli katta loyihada bu farq — har kuni yo'qotiladigan yoki tejaladigan soatlar demakdir. Spotify Bazel'ga o'tgach, iOS'da birlashtirishdan oldingi (pre-merge) kutish vaqti "to'rt barobar" qisqargan (InfoQ, 2023-yil oktabr) — buning katta qismi aynan shu keshlangan, faqat o'zgarganini qayta ishlaydigan test modelidan.

Shu darsda biz kichik, lekin real test qilsa bo'ladigan loyiha quramiz: "Kutubxona" — kitoblar ro'yxatini boshqaradigan oddiy ilova. Uning mantig'i (KutubxonaKit) toza Swift kodida bo'ladi, shuning uchun uni tez va mustaqil test qilish mumkin. Keyin ilovaning o'ziga hosted test va UI test yozamiz.


2. Test anatomiyasi: uch rule {#anatomiya}

Birinchi tuzoq ko'pchilikni shu yerda kutadi: Bazel'da test kodini **asosiy kod bilan bir swift_library ichiga qo'shib bo'lmaydi.** Test kodi XCTest yoki Testing framework'iga bog'liq, bu framework'lar esa faqat test kontekstida mavjud. Shuning uchun struktura doim uch qismdan iborat bo'ladi:

Test target anatomiyasi: asosiy kod swift_library, test kodi testonly swift_library, ios_unit_test ularni .xctest bundle'ga yig'adi va simulyatorda ishga tushiradi

Yuqoridagi rasm uch rule'ni va ular hosil qilgan quvurni ko'rsatadi: ikkita swift_library (asosiy kod + test kodi) deps orqali bog'lanadi, ios_unit_test esa test kodidan .xctest bundle quradi va uni ios_xctestrun_runner orqali simulyatorda ishga tushiradi.

  1. Asosiy kod — oddiy swift_library (test haqida hech narsa bilmaydi).
  2. Test kodi — alohida swift_library, lekin testonly = True bayrog'i bilan va asosiy kodga deps orqali bog'langan.
  3. Test target — ios_unit_test, u test kutubxonasidan .xctest bundle quradi va ishga tushiradi.

testonly = True nima qiladi? U Bazel'ga: "bu target faqat testlar uchun; uni ishlab chiqarish (production) kodiga deps qilib qo'yib bo'lmaydi" deb aytadi. Agar kimdir xato bilan test kutubxonasini ios_applicationning depsiga qo'shsa, Bazel buildni rad etadi. Bu — arxitekturaviy himoya: test yordamchi kodingiz hech qachon ilovaga sizib kirmaydi.

🧠 Esda tuting: "Nega test kodini alohida swift_libraryga ajratamiz?" degan savolga aniq javob: XCTest/Testing import'lari faqat test target'ida mavjud; testonly esa ularni production grafidan ajratib turadi. Asosiy kod → test kodi → ios_unit_test — bu uchlik har bir modul uchun takrorlanadi (keyingi darslarda uni bitta macro'ga yig'amiz).

Loyihamizning papka tuzilishi:

kutubxona/
├── .bazelversion
├── .bazelrc
├── MODULE.bazel
├── BUILD.bazel              # bo'sh — workspace ildizini package qiladi
├── KutubxonaKit/
│   ├── BUILD.bazel
│   ├── Sources/
│   │   ├── Book.swift
│   │   └── BookLibrary.swift
│   └── Tests/
│       ├── BookTests.swift            # XCTest
│       └── BookLibrarySwiftTests.swift # Swift Testing
└── App/
    ├── BUILD.bazel
    ├── Info.plist
    ├── Sources/
    │   ├── KutubxonaApp.swift
    │   └── ContentView.swift
    ├── HostedTests/
    │   └── HostedTests.swift
    └── UITests/
        └── KutubxonaUITests.swift

MODULE.bazel — 5-darsdagidek, uchta dependency (apple_support eng tepada turishi kerak, chunki birinchi ro'yxatdan o'tgan cc toolchain yutadi):

# MODULE.bazel
module(name = "kutubxona", version = "1.0.0")

bazel_dep(name = "apple_support", version = "2.10.1")
bazel_dep(name = "rules_swift", version = "4.2.1")
bazel_dep(name = "rules_apple", version = "5.3.0")

3. Birinchi test: XCTest bilan {#birinchi-test}

Sinaladigan kod

Avval test qiladigan narsamiz bo'lsin. KutubxonaKit ichida ikkita fayl: Book modeli va BookLibrary — kitoblarni qo'shish, qidirish, saralash mantig'i. Diqqat qiling: bu kod toza — faqat Foundation'ga bog'liq, UIKit/SwiftUI yo'q. Aynan shuning uchun uni eng tez test turida (logic test) sinash mumkin.

// KutubxonaKit/Sources/Book.swift
import Foundation

// Bitta kitob. Barcha maydonlar o'zgarmas (immutable) — test qilish osonroq.
public struct Book: Equatable, Identifiable, Sendable {
    public let id: Int
    public let title: String
    public let author: String
    public let year: Int
    public let pages: Int
    public let isRead: Bool

    public init(
        id: Int,
        title: String,
        author: String,
        year: Int,
        pages: Int,
        isRead: Bool = false
    ) {
        self.id = id
        self.title = title
        self.author = author
        self.year = year
        self.pages = pages
        self.isRead = isRead
    }
}
// KutubxonaKit/Sources/BookLibrary.swift
import Foundation

// Kitoblarni qo'shish, qidirish, saralash logikasi. Bu — toza (pure) logika:
// Foundation'dan tashqari hech narsaga bog'liq emas.
public struct BookLibrary: Sendable {
    public private(set) var books: [Book]

    public init(books: [Book] = []) {
        self.books = books
    }

    public enum LibraryError: Error, Equatable {
        case duplicateID(Int)
        case emptyTitle
    }

    // Validatsiya bilan qo'shish: bir xil id yoki bo'sh sarlavha — xato.
    public mutating func add(_ book: Book) throws {
        guard !book.title.trimmingCharacters(in: .whitespaces).isEmpty else {
            throw LibraryError.emptyTitle
        }
        guard !books.contains(where: { $0.id == book.id }) else {
            throw LibraryError.duplicateID(book.id)
        }
        books.append(book)
    }

    // Sarlavha yoki muallif bo'yicha qidirish (katta-kichik harf farqsiz).
    public func search(_ query: String) -> [Book] {
        let needle = query.trimmingCharacters(in: .whitespaces).lowercased()
        guard !needle.isEmpty else { return books }
        return books.filter {
            $0.title.lowercased().contains(needle)
                || $0.author.lowercased().contains(needle)
        }
    }

    public func unread() -> [Book] {
        books.filter { !$0.isRead }
    }

    public func sortedByYear(ascending: Bool = true) -> [Book] {
        books.sorted { ascending ? $0.year < $1.year : $0.year > $1.year }
    }

    public var totalPages: Int {
        books.reduce(0) { $0 + $1.pages }
    }
}

Test kodi (XCTest)

Endi XCTest testlarini yozamiz. XCTest — Apple'ning klassik test framework'i: XCTestCase'dan meros olasiz, metod nomlari test bilan boshlanadi, tekshiruvlar XCTAssert... funksiyalari bilan yoziladi.

// KutubxonaKit/Tests/BookTests.swift
import XCTest
@testable import KutubxonaKit

final class BookLibraryXCTests: XCTestCase {

    private func namuna() -> BookLibrary {
        BookLibrary(books: [
            Book(id: 1, title: "O'tkan kunlar", author: "Abdulla Qodiriy", year: 1925, pages: 368, isRead: true),
            Book(id: 2, title: "Mehrobdan chayon", author: "Abdulla Qodiriy", year: 1929, pages: 320),
            Book(id: 3, title: "Shum bola", author: "G'afur G'ulom", year: 1936, pages: 160),
        ])
    }

    func testTotalPages() {
        XCTAssertEqual(namuna().totalPages, 848)
    }

    func testUnreadFiltrlaydi() {
        let unread = namuna().unread()
        XCTAssertEqual(unread.count, 2)
        XCTAssertFalse(unread.contains { $0.isRead })
    }

    func testSearchMuallifBoyicha() {
        let topildi = namuna().search("qodiriy")
        XCTAssertEqual(topildi.count, 2)
    }

    func testSortedByYear() {
        let yillar = namuna().sortedByYear().map(\.year)
        XCTAssertEqual(yillar, [1925, 1929, 1936])
    }

    func testAddDuplicateIDxatoBeradi() {
        var lib = namuna()
        XCTAssertThrowsError(try lib.add(Book(id: 1, title: "Yana bir", author: "X", year: 2000, pages: 100))) { error in
            XCTAssertEqual(error as? BookLibrary.LibraryError, .duplicateID(1))
        }
    }

    func testAddBoshSarlavhaXatoBeradi() {
        var lib = namuna()
        XCTAssertThrowsError(try lib.add(Book(id: 99, title: "   ", author: "X", year: 2000, pages: 100))) { error in
            XCTAssertEqual(error as? BookLibrary.LibraryError, .emptyTitle)
        }
    }
}

@testable import KutubxonaKit — bu diqqatga sazovor: @testable modulning internal darajadagi a'zolariga ham kirish beradi (bizda hammasi public, lekin odatda test uchun internal kod ham ko'rinishi kerak bo'ladi). Buning ishlashi uchun asosiy kutubxona debug rejimida (-enable-testing bilan) kompilyatsiya qilinishi kerak — Bazel buni test grafigida avtomatik qiladi.

BUILD fayli

Mana o'sha "uch rule" amalda:

# KutubxonaKit/BUILD.bazel
load("@rules_swift//swift:swift.bzl", "swift_library")
load("@rules_apple//apple:ios.bzl", "ios_unit_test")

# 1. Sinaladigan kod
swift_library(
    name = "KutubxonaKit",
    srcs = glob(["Sources/**/*.swift"]),
    module_name = "KutubxonaKit",
    visibility = ["//visibility:public"],
)

# 2. Test kodi alohida kutubxonada: testonly = True
swift_library(
    name = "KutubxonaKitTestsLib",
    testonly = True,
    srcs = glob(["Tests/**/*.swift"]),
    deps = [":KutubxonaKit"],
)

# 3. Test target'i: .xctest bundle quradi va ishga tushiradi
ios_unit_test(
    name = "KutubxonaKitTests",
    minimum_os_version = "17.0",
    deps = [":KutubxonaKitTestsLib"],
)

ios_unit_testning majburiy atributlari kam: name va minimum_os_version. deps — test kutubxonasi. test_host bermaganimiz uchun bu logic test bo'ladi — ya'ni ilovasiz, mustaqil ishlaydi (bu haqda 9-bo'limda batafsil). Rule'ning to'liq atributlari ro'yxati rules_apple 5.3.0 hujjatlaridagi ios_unit_test bo'limida (deps, data, env, size, timeout, test_filter, test_host, shard_count, runner, collect_code_coverage...).

Avval shunchaki quramiz (hali ishga tushirmaymiz):

bazel build //KutubxonaKit:KutubxonaKitTests
INFO: Analyzed target //KutubxonaKit:KutubxonaKitTests (200 packages loaded, 7850 targets configured).
[340 / 352] Compiling Swift module //KutubxonaKit:KutubxonaKit; 1s disk-cache, worker
[342 / 352] Compiling Swift module //KutubxonaKit:KutubxonaKitTestsLib; 0s disk-cache, worker
[348 / 352] Bundling KutubxonaKitTests.__internal__.__test_bundle; 0s disk-cache, darwin-sandbox
Target //KutubxonaKit:KutubxonaKitTests up-to-date:
  bazel-bin/KutubxonaKit/KutubxonaKitTests
  bazel-out/ios_sim_arm64-fastbuild-ios-sim_arm64-min17.0-ST-.../bin/KutubxonaKit/KutubxonaKitTests.zip
INFO: Elapsed time: 29.168s, Critical Path: 8.48s
INFO: 352 processes: 174 disk cache hit, 169 internal, 6 darwin-sandbox, 1 local, 2 worker.

E'tibor bering: bazel build test'ni qurdi, lekin ishga tushirmadi — .xctest bundle bazel-bin'da tayyor turibdi. Testni haqiqatda ishga tushirish uchun bazel build emas, bazel test kerak. Lekin avval .bazelrc'ga bir muhim sozlamani qo'shishimiz kerak, aks holda test simulyator xatosi bilan yiqiladi — buni 8-bo'limda hal qilamiz. Hozircha keling, Swift Testing'ni ko'rib chiqaylik.


4. Swift Testing: zamonaviy sintaksis {#swift-testing}

XCTest 2015-yildan beri bor va hamma joyda ishlaydi. 2024-yilda Apple Swift Testing — yangi, zamonaviy test framework'ini chiqardi: XCTestCase merosi yo'q, oddiy @Test funksiyalari; XCTAssertEqual o'rniga oddiy #expect(a == b); suite'lar oddiy struct. Eng muhimi — Bazel bulardan birini tanlashga majbur qilmaydi: **ikkalasini bitta ios_unit_testda aralashtirib yozsangiz ham ishlaydi.**

Mana o'sha BookLibrary uchun Swift Testing testlari:

// KutubxonaKit/Tests/BookLibrarySwiftTests.swift
import Testing
@testable import KutubxonaKit

@Suite("BookLibrary — Swift Testing")
struct BookLibrarySwiftTests {

    private func namuna() -> BookLibrary {
        BookLibrary(books: [
            Book(id: 1, title: "O'tkan kunlar", author: "Abdulla Qodiriy", year: 1925, pages: 368, isRead: true),
            Book(id: 2, title: "Mehrobdan chayon", author: "Abdulla Qodiriy", year: 1929, pages: 320),
            Book(id: 3, title: "Shum bola", author: "G'afur G'ulom", year: 1936, pages: 160),
        ])
    }

    @Test("Umumiy sahifalar soni to'g'ri hisoblanadi")
    func totalPages() {
        #expect(namuna().totalPages == 848)
    }

    @Test("O'qilmagan kitoblar faqat isRead == false bo'lganlari")
    func unread() {
        let unread = namuna().unread()
        #expect(unread.count == 2)
        #expect(!unread.contains { $0.isRead })
    }

    @Test("Bo'sh sarlavha .emptyTitle xatosini beradi")
    func emptyTitleThrows() throws {
        var lib = namuna()
        #expect(throws: BookLibrary.LibraryError.emptyTitle) {
            try lib.add(Book(id: 50, title: "  ", author: "X", year: 2001, pages: 50))
        }
    }

    // Parametrlashtirilgan test: bitta funksiya, uchta kirish/natija jufti.
    @Test("Qidiruv bir nechta so'rov uchun to'g'ri soni qaytaradi", arguments: [
        ("qodiriy", 2),
        ("shum", 1),
        ("tolkien", 0),
    ])
    func searchCount(query: String, kutilgan: Int) {
        #expect(namuna().search(query).count == kutilgan)
    }
}

Nimalarga e'tibor berish kerak:

  • @Suite("...") — testlar guruhi (ixtiyoriy nom bilan). Oddiy struct, meros yo'q.
  • @Test("...") — bitta test. Nomi — o'qilishi oson matn.
  • #expect(...) — tekshiruv. XCTAssertEqual(a, b) o'rniga oddiy #expect(a == b). Agar yiqilsa, Swift Testing ifodaning ikki tomonidagi qiymatlarni ko'rsatadi.
  • #expect(throws: ...) — xato kutilishini tekshiradi (XCTAssertThrowsError ekvivalenti).
  • @Test(arguments: [...]) — parametrlashtirilgan test: bitta funksiya har bir argument jufti uchun qayta chaqiriladi. Uchta holat uchun uchta alohida test yozish shart emas.

BUILD faylini o'zgartirishning hojati yo'q: glob(["Tests/**/*.swift"]) yangi faylni ham o'z ichiga oladi. Test kutubxonasi endi ikki faylli: biri XCTest, biri Swift Testing. ios_unit_test ikkalasini bitta .xctest bundle'ga qo'shadi va ikkala framework'ni ham ishga tushiradi.

🧠 Esda tuting: rules_swift Swift Testing'ni tug'ma qo'llaydi — uning ichida tools/test_observer/SwiftTestingRunner.swift test observer'i bor. Siz hech qanday qo'shimcha dependency yoki bayroq qo'shishingiz shart emas; faqat import Testing yozing. (Bu bizning mashinada rules_swift 4.2.1 + Xcode 27.1 bilan tekshirilgan.)

Natijani 5-bo'limda ishga tushirganimizda ko'rasiz — bitta buyruq ikkala framework summary'sini ham chiqaradi.


5. Testlarni ishga tushirish va filtrlash {#ishga-tushirish}

bazel build test'ni quradi; bazel test esa quradi va ishga tushiradi. (Avval .bazelrc'ga simulyator sozlamasini qo'shgan bo'lishingiz kerak — 8-bo'limga qarang; mana shu misollar o'sha sozlama bilan ishlaydi.)

bazel test //KutubxonaKit:KutubxonaKitTests --test_output=all

--test_output=all test'ning to'liq chiqishini ko'rsatadi. Mana haqiqiy natija (qisqartirilgan), avval XCTest, keyin Swift Testing:

Test Suite 'BookLibraryXCTests' started at 2026-10-13 12:42:44.621.
Test Case '-[...BookLibraryXCTests testTotalPages]' passed (0.000 seconds).
Test Case '-[...BookLibraryXCTests testUnreadFiltrlaydi]' passed (0.000 seconds).
...
	 Executed 6 tests, with 0 failures (0 unexpected) in 0.013 (0.014) seconds
◇ Test run started.
↳ Testing Library Version: 2084
↳ Target Platform: arm64-apple-ios17.0-simulator
◇ Suite "BookLibrary — Swift Testing" started.
✔ Test "Umumiy sahifalar soni to'g'ri hisoblanadi" passed after 0.001 seconds.
◇ Test case passing 2 arguments query → "qodiriy", kutilgan → 2 to "Qidiruv ..." started.
✔ Test "Qidiruv bir nechta so'rov uchun to'g'ri soni qaytaradi" with 3 test cases passed after 0.001 seconds.
✔ Suite "BookLibrary — Swift Testing" passed after 0.005 seconds.
✔ Test run with 4 tests in 1 suite passed after 0.005 seconds.
Testing is serialized
//KutubxonaKit:KutubxonaKitTests                                         PASSED in 1.8s

Executed 1 out of 1 test: 1 test passes.

Ko'rib turganingizdek, ikkala framework ham ishladi: XCTest "Executed 6 tests", Swift Testing "Test run with 4 tests in 1 suite passed" (parametrlashtirilgan test "with 3 test cases" deb sanaldi). Bazel esa butun .xctest bundle'ni bitta test target sifatida ko'radi: PASSED in 1.8s.

--test_output rejimlari

  • --test_output=summary (standart) — faqat o'tdi/yiqildi va log fayl manzili.
  • --test_output=errors — faqat yiqilgan testlarning logini chiqaradi. CI uchun ideal: muvaffaqiyatli testlar shovqin qilmaydi.
  • --test_output=all — hamma narsani chiqaradi (yuqoridagi kabi).
  • --test_output=streamed — testni real vaqtda, oqim ko'rinishida ko'rsatadi (bitta test, debug uchun; parallel ishlashni o'chiradi).

Odatda .bazelrc'ga test --test_output=errors yozib qo'yiladi — biz ham shunday qildik.

Bitta testni ishga tushirish: --test_filter

Butun to'plamni emas, bitta metodni ishga tushirmoqchimisiz? --test_filter buni qiladi. Format — KlassNomi/metodNomi:

bazel test //KutubxonaKit:KutubxonaKitTests \
  --test_filter=BookLibraryXCTests/testTotalPages \
  --test_output=all --nocache_test_results
Test Case '-[...BookLibraryXCTests testTotalPages]' started.
Test Case '-[...BookLibraryXCTests testTotalPages]' passed (0.002 seconds).
	 Executed 1 test, with 0 failures (0 unexpected) in 0.002 (0.004) seconds
//KutubxonaKit:KutubxonaKitTests                                         PASSED in 9.4s
Executed 1 out of 1 test: 1 test passes.

Faqat bitta test ishladi ("Executed 1 test"). --test_filter regular expression ham qabul qiladi: --test_filter='BookLibraryXCTests/test(Total|Unread).*' ikkala metodni ishga tushiradi. Bu filter test runner'ga uzatiladi — ya'ni bundle baribir to'liq quriladi, lekin faqat tanlangan metodlar bajariladi.

💡 Pro tip: PASSED in 9.4s — nega bu safar sekin? Chunki --nocache_test_results keshni o'chirdi va test simulyatorda yangidan ishga tushdi (simulyator bilan muloqot vaqtni oshiradi). Keshdan kelganda esa, hozir ko'ramiz, bu 0.2 soniya bo'ladi.

Test yiqilganda nima bo'ladi?

Keling, ataylab bitta testni buzamiz — testTotalPages'da 848 o'rniga 999 kutamiz — va --test_output=errors bilan ishga tushiramiz:

BookTests.swift:16: error: -[...BookLibraryXCTests testTotalPages] : XCTAssertEqual failed: ("848") is not equal to ("999")
Test Case '-[...BookLibraryXCTests testTotalPages]' failed (0.053 seconds).
	 Executed 6 tests, with 1 failure (0 unexpected) in 0.056 seconds
//KutubxonaKit:KutubxonaKitTests                                         FAILED in 4.3s

Executed 1 out of 1 test: 1 fails locally.

Bazel aniq ko'rsatadi: qaysi fayl va qator (BookTests.swift:16), nima kutilgan (848), nima bo'lgan (999). Target FAILED bo'ldi va bazel test nol bo'lmagan exit code bilan tugaydi — CI buni to'xtatish uchun ishlatadi.


6. Test natijasi keshi — Bazel'ning super-kuchi {#test-kesh}

Mana shu — Bazel testlarini boshqa test yurituvchilardan ajratib turadigan asosiy narsa. Esingizdami, Bazel har action uchun action key hisoblaydi (3-darsda ko'rgan edik). Test ham action: uning key'i test binary, runner, kirish fayllari va muhim bayroqlardan hisoblanadi. Agar bu key o'zgarmagan bo'lsa, natija ham o'zgarmaydi — demak testni qaytadan ishga tushirishning ma'nosi yo'q.

Test keshi qarori: action key kirishlar o'zgarmasa, test qayta ishlamaydi va (cached) PASSED beradi; o'zgarsa, simulyatorda ishga tushadi

Buni o'z ko'zingiz bilan ko'ring. Birinchi marta ishga tushiramiz:

bazel test //KutubxonaKit:KutubxonaKitTests
//KutubxonaKit:KutubxonaKitTests                                         PASSED in 1.8s
Executed 1 out of 1 test: 1 test passes.

Endi hech narsani o'zgartirmay, yana ishga tushiramiz:

bazel test //KutubxonaKit:KutubxonaKitTests
INFO: Elapsed time: 0.188s, Critical Path: 0.02s
INFO: 1 process: 1 action cache hit, 1 internal.
//KutubxonaKit:KutubxonaKitTests                                (cached) PASSED in 1.8s

Executed 0 out of 1 test: 1 test passes.

Ikki narsaga qarang:

  1. **(cached) PASSED** — test simulyatorda umuman ishlamadi; Bazel oldingi natijani keshdan oldi.
  2. **Executed 0 out of 1 test — nol test bajarildi, lekin baribir "1 test passes". Butun jarayon 0.188 soniya** davom etdi (birinchi martadagi bir necha soniya o'rniga).

📊 Real raqamlar: Bizning kichik to'plamimizda (10 test) birinchi ishga tushish — PASSED in 1.8s; keshdan — butun buyruq 0.188s. Ya'ni kesh testni ~10 barobar tez "o'tkazdi", chunki aslida u umuman ishlamadi. Minglab testli loyihada bu farq soatlab CI vaqtini tejaydi. (O'lchov: M4 Pro, Xcode 27.1, Bazel 9.3.0, iPhone 17 Pro / iOS 27.0.)

Keshni nima buzadi?

Kesh "jim turib qolgan eski natija"ni bermaydi — u faqat hech narsa o'zgarmaganda ishlaydi. Quyidagilardan birortasi test key'ini o'zgartiradi va testni qaytadan ishga tushiradi:

  • test faylini yoki sinaladigan kodni o'zgartirish (BookLibrary.swiftni tahrirlasangiz, test qayta ishlaydi);
  • test bayroqlarini o'zgartirish (--test_filter, --test_env, --test_arg);
  • test muhiti (env), data fayllari;
  • simulyator yoki Xcode versiyasini o'zgartirish.

Keshni ataylab chetlab o'tish

Ba'zan testni majburan qayta ishga tushirishingiz kerak bo'ladi — masalan, flaky (beqaror) testni ushlash uchun:

# Har safar qayta ishga tushiradi, keshni mutlaqo e'tiborsiz qoldiradi
bazel test //KutubxonaKit:KutubxonaKitTests --nocache_test_results
//KutubxonaKit:KutubxonaKitTests                                         PASSED in 2.9s
Executed 1 out of 1 test: 1 test passes.

Endi (cached) yo'q va "Executed 1 out of 1" — test haqiqatda qayta ishladi. --cache_test_results=no ham xuddi shunday.

⚠️ Diqqat: Test keshi juda foydali, lekin u faqat hermetik (yopiq) testlar uchun to'g'ri ishlaydi. Agar testingiz tashqi holatga bog'liq bo'lsa — tarmoq, joriy vaqt (Date()), fayl tizimidagi global papka, tasodifiy son — kesh sizni aldashi mumkin: kod o'zgarmagan, lekin natija boshqacha bo'lishi mumkin edi. Testlarni hermetik yozing (kirishlarni in'ektsiya qiling), aks holda (cached) PASSED yolg'on xotirjamlik beradi.


7. size, timeout va flaky testlar {#size-flaky}

Testlarni ishga tushirganingizda shunday ogohlantirish ko'rgandirsiz:

There were tests whose specified size is too big. Use the
--test_verbose_timeout_warnings command line option to see which ones these are.

Bu nima? Bazel har test target'iga **size** beradi (standart — medium). size ikki narsani belgilaydi: test qancha resurs talab qiladi va uning timeout'i qancha. Bizning testlar 2 soniyada o'tyapti, lekin medium size timeout'i 300 soniya — Bazel "sizning testingiz juda katta size'da e'lon qilingan, aslida u juda tez" deb ogohlantiryapti.

To'g'rilaymiz — ios_unit_testga size = "small" qo'shamiz:

# KutubxonaKit/BUILD.bazel (yangilangan)
ios_unit_test(
    name = "KutubxonaKitTests",
    size = "small",            # tez logic testlar uchun
    minimum_os_version = "17.0",
    deps = [":KutubxonaKitTestsLib"],
)

size qiymatlari va ularning standart timeout'lari (Bazel hujjatlaridan):

sizeStandart timeoutQachon
small60 s (short)Tez logic/unit testlar — ko'pchiligi shu
medium300 s (moderate)Simulyator yoki disk bilan ishlaydiganlar
large900 s (long)Og'ir integratsiya/UI testlar
enormous3600 s (eternal)Juda kam — butun oqim testlari

timeout'ni size'dan alohida ham berish mumkin: timeout = "short" (60 s), "moderate" (300 s), "long" (900 s), "eternal" (3600 s). Qoida: testingizga mos eng kichik size'ni tanlang — shunda haqiqatan osilib qolgan test tez aniqlanadi, 15 daqiqa kutib o'tirmaysiz.

Flaky (beqaror) testlar

Ba'zi testlar ba'zan o'tadi, ba'zan yiqiladi — kod o'zgarmasa ham. Bu flaky testlar: odatda vaqt poygasi (race), tartibga bog'liqlik yoki tashqi holat sabab. Bazel ularni boshqarish uchun bir nechta vosita beradi.

Flaky testni ushlash — bitta testni ko'p marta ishga tushiring:

# Bitta testni ketma-ket 20 marta ishga tushiradi; bittasi ham yiqilsa — muammo bor
bazel test //KutubxonaKit:KutubxonaKitTests \
  --test_filter=BookLibraryXCTests/testSortedByYear \
  --runs_per_test=20

Agar 20 tadan bittasi yiqilsa, sizda flaky test bor. --runs_per_test=N kesh bilan ishlamaydi (har safar haqiqatan ishga tushadi) — bu ataylab shunday.

Flaky deb belgilash — vaqtincha chora sifatida, testni flaky = True qilib qo'yish mumkin:

ios_unit_test(
    name = "KutubxonaKitTests",
    size = "small",
    flaky = True,              # yiqilsa, Bazel 3 martagacha qayta uriadi
    minimum_os_version = "17.0",
    deps = [":KutubxonaKitTestsLib"],
)

flaky = True bo'lsa, test yiqilganda Bazel uni 3 martagacha qayta uriadi; agar bir marta o'tsa, "PASSED" (lekin "flaky" deb belgilaydi). --flaky_test_attempts=N bilan urinishlar sonini o'zgartirasiz.

⚠️ Diqqat: flaky = True — bu muammoni yashirish, hal qilish emas. U faqat vaqtincha, siz asl sababni (race, tartib, tashqi holat) topguningizcha ishlatiladi. Doimiy flaky = True — texnik qarz: beqaror test ertami-kechmi haqiqiy xatoni o'tkazib yuboradi.

Sharding — bitta test target'ini bo'laklarga bo'lish

Agar bitta target'da yuzlab test bo'lsa, ularni parallel ishga tushirish uchun sharding ishlatiladi:

ios_unit_test(
    name = "KutubxonaKitTests",
    size = "small",
    shard_count = 4,           # testlarni 4 bo'lakka bo'lib, parallel ishga tushiradi
    minimum_os_version = "17.0",
    deps = [":KutubxonaKitTestsLib"],
)

shard_count = 4 bo'lsa, Bazel test'ni 4 ta shard sifatida ishga tushiradi (har biri testlarning bir qismini oladi), va ular bir-biriga parallel ishlashi mumkin. Bizning 10 testli kichik to'plamimizga bu kerak emas — lekin 500 testli modulda bu sezilarli tezlanish beradi. (shard_count — kichik modullar uchun ortiqcha; faqat test soni ko'p bo'lganda qo'shing.)


8. Simulyatorni tanlash va Xcode 27 tuzog'i {#simulyator}

Endi ochig'ini aytamiz: agar 3-bo'limdagi ios_unit_testni to'g'ridan-to'g'ri, hech qanday sozlamasiz ishga tushirsangiz, Xcode 27 muhitida u yiqiladi. Bu — har bir yangi boshlovchini kutib turgan tuzoq, shuning uchun sababini tushunaylik.

ios_unit_test standart holda ios_xctestrun_runner'dan foydalanadi (rules_apple 5.0'dan beri standart runner shu). Bu runner test uchun simulyator tanlaydi. U qurilmani quyidagi tartibda qidiradi (rules_apple 5.3.0 manbasidan):

  1. test target'ining device_type/os_version atributlari (agar berilgan bo'lsa);
  2. --@rules_apple//apple/build_settings:ios_simulator_device / ...ios_simulator_version build sozlamalari;
  3. native Bazel bayroqlari --ios_simulator_device / --ios_simulator_version (manbadagi izoh: "Bazel 9.x qo'llab-quvvatlashni tashlaganimizda olib tashlanadi");
  4. hech biri berilmasa — qattiq kodlangan "iPhone 15".

Muammo shundaki, Xcode 27 Apple Silicon'da "iPhone 15" simulyatori yo'q (eng eski oddiy model boshqacha), shuning uchun 4-variant simctl create ... Incompatible device xatosi bilan yiqiladi. (Bu bizning mashinamizda tekshirildi.)

Yechim — 2-variantni ishlatish, ya'ni .bazelrc'da build sozlamasi orqali o'z simulyatoringizni belgilash. Bu forma eng kelajakka chidamli (native bayroqlar vaqti kelib olib tashlanadi):

# .bazelrc
build --disk_cache=~/.cache/bazel-disk

# Xcode 27.x: standart runner "iPhone 15" so'raydi va yiqiladi.
# O'zimizning simulyatorni build sozlamasi orqali tanlaymiz.
test --@rules_apple//apple/build_settings:ios_simulator_device="iPhone 17 Pro"
test --@rules_apple//apple/build_settings:ios_simulator_version=27.0

test --test_output=errors

⚠️ Diqqat: Qaysi simulyatorlar mavjudligini o'zingiz tekshiring — "iPhone 17 Pro" va "27.0" bizning mashinada bor, sizda boshqacha bo'lishi mumkin:

bash xcrun simctl list devicetypes | grep iPhone xcrun simctl list runtimes | grep iOS

Mos qurilma va versiyani tanlab, .bazelrc'ga yozing.

Birinchi marta test ishga tushganda, runner BAZEL_TEST_iPhone 17 Pro_27.0 nomli doimiy simulyator yaratadi va uni keyingi testlarda qayta ishlatadi (reuse_simulator standart holda True). Shuning uchun birinchi test sekinroq (simulyator yaratiladi va yuklanadi), keyingilar tezroq.

Test tartibini tasodifiylashtirish

Agar testlaringiz bir-biriga yashirin bog'liq bo'lsa (biri ikkinchisi qoldirgan holatga tayansa), ular doim bir tartibda ishlagani uchun buni sezmaysiz. ios_xctestrun_random_runner testlarni tasodifiy tartibda ishga tushiradi va shunday bog'liqliklarni fosh qiladi:

ios_unit_test(
    name = "KutubxonaKitTests",
    size = "small",
    minimum_os_version = "17.0",
    runner = "@rules_apple//apple/testing/default_runner:ios_xctestrun_random_runner",
    deps = [":KutubxonaKitTestsLib"],
)

Tartibli runner ham bor (ios_xctestrun_ordered_runner) — bu standart (tasodifiy emas) xatti-harakat.


9. Logic test va hosted test (test_host) {#hosted}

Hozirgacha yozgan testlarimiz logic test (library test) edi: test_host berilmagan, shuning uchun test ilovasiz, mustaqil ishlaydi. Bu eng tez tur. Lekin uning cheklovi bor: ilova konteksti yo'q. Bundle.main test bundle'iga ishora qiladi, ilovaga emas; UIApplication, ba'zi NSUserDefaults xatti-harakatlari mavjud emas.

Ba'zan sizga ilova ichida ishlaydigan test kerak — masalan, ilova bundle'idagi resursni o'qiydigan yoki ilova holatiga bog'liq kodni sinaydigan test. Bu — hosted test: test_host atributiga ilovani berasiz, va test o'sha ilovaning jarayonida (process) ishlaydi.

Avval sinaladigan ilova kerak. App/ papkasida oddiy SwiftUI ilova (5-darsdagi kabi):

// App/Sources/KutubxonaApp.swift
import SwiftUI
import KutubxonaKit

@main
struct KutubxonaApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView(library: BookLibrary(books: [
                Book(id: 1, title: "O'tkan kunlar", author: "Abdulla Qodiriy", year: 1925, pages: 368, isRead: true),
                Book(id: 2, title: "Mehrobdan chayon", author: "Abdulla Qodiriy", year: 1929, pages: 320),
                Book(id: 3, title: "Shum bola", author: "G'afur G'ulom", year: 1936, pages: 160),
            ]))
        }
    }
}
// App/Sources/ContentView.swift
import SwiftUI
import KutubxonaKit

struct ContentView: View {
    let library: BookLibrary

    var body: some View {
        NavigationStack {
            List(library.sortedByYear()) { book in
                VStack(alignment: .leading, spacing: 4) {
                    Text(book.title).font(.headline)
                    Text("\(book.author) · \(book.year)")
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                }
                // UI test shu identifikator orqali qatorni topadi (10-bo'lim)
                .accessibilityIdentifier("book-\(book.id)")
            }
            .navigationTitle("Kutubxona")
        }
    }
}

Hosted test — Bundle.mainning endi ilova bundle'iga ishora qilishini tekshiramiz:

// App/HostedTests/HostedTests.swift
import XCTest
@testable import KutubxonaKit

final class HostedTests: XCTestCase {

    func testIlovaBundleIDmavjud() {
        // Logic (test_host'siz) testda bu nil yoki test bundle bo'lardi.
        let bundleID = Bundle.main.bundleIdentifier
        XCTAssertEqual(bundleID, "uz.azimjondev.kutubxona")
    }

    func testKutubxonaKitHostKontekstidaIshlaydi() {
        let lib = BookLibrary(books: [
            Book(id: 1, title: "O'tkan kunlar", author: "Abdulla Qodiriy", year: 1925, pages: 368),
        ])
        XCTAssertEqual(lib.totalPages, 368)
    }
}

BUILD fayli — asosiy farq test_host = ":Kutubxona":

# App/BUILD.bazel
load("@rules_swift//swift:swift.bzl", "swift_library")
load(
    "@rules_apple//apple:ios.bzl",
    "ios_application",
    "ios_ui_test",
    "ios_unit_test",
)

swift_library(
    name = "AppLib",
    srcs = glob(["Sources/**/*.swift"]),
    deps = ["//KutubxonaKit"],
)

ios_application(
    name = "Kutubxona",
    bundle_id = "uz.azimjondev.kutubxona",
    families = ["iphone"],
    infoplists = ["Info.plist"],
    minimum_os_version = "17.0",
    deps = [":AppLib"],
)

# Hosted unit test: ilova jarayonida ishlaydi
swift_library(
    name = "HostedTestsLib",
    testonly = True,
    srcs = glob(["HostedTests/**/*.swift"]),
    deps = ["//KutubxonaKit"],
)

ios_unit_test(
    name = "HostedTests",
    minimum_os_version = "17.0",
    test_host = ":Kutubxona",     # <-- test ilova ichida ishlaydi
    deps = [":HostedTestsLib"],
)

Ishga tushiramiz:

bazel test //App:HostedTests
** TEST EXECUTE SUCCEEDED **
//App:HostedTests                                                        PASSED in 17.7s
Executed 1 out of 1 test: 1 test passes.

📊 Real raqamlar: Logic test PASSED in 1.8s, hosted test esa PASSED in 17.7s — deyarli 10 barobar sekinroq. Sababi: hosted test to'liq ilovani simulyatorda ishga tushiradi va xcodebuild'dan foydalanadi (log'da IDETestOperationsObserverDebug qatorlari ko'rinadi), logic test esa ilovasiz, to'g'ridan-to'g'ri xctest bilan ishlaydi. (O'lchov: M4 Pro, Xcode 27.1, iPhone 17 Pro / iOS 27.0.)

💡 Pro tip: Shuning uchun qoida oddiy: imkon qadar logic test yozing. Toza logikani (model, hisob-kitob, validatsiya) test_hostsiz sinang — bu tez va ishonchli. test_hostni faqat haqiqatan ilova konteksti kerak bo'lganda qo'shing. Minglab testli loyihada bu farq CI vaqtini belgilaydi.


10. UI testlar va bir timeout hikoyasi {#ui-test}

UI test — eng yuqori daraja: u ilovani foydalanuvchi kabi boshqaradi (tugmalarni bosadi, matnlarni o'qiydi, ekranlar orasida yuradi). Bazel'da bu ios_ui_test rule'i bilan yoziladi va u ham test_hostni talab qiladi (boshqariladigan ilova).

Avval ilovamizni test qilish uchun unga belgilar qo'yamiz — ContentView'da har qatorga .accessibilityIdentifier("book-\(book.id)") qo'ygan edik, va navigationTitle("Kutubxona"). UI test shulardan foydalanadi.

Mana bu — birinchi urinishimiz, va u mantiqan to'g'ri ko'rinadi:

// App/UITests/KutubxonaUITests.swift — BIRINCHI URINISH (yiqiladi!)
import XCTest

// UI test: XCUIApplication ilovani simulyatorda ishga tushiradi va ekran
// elementlari bilan foydalanuvchi kabi ishlaydi.
final class KutubxonaUITests: XCTestCase {

    func testRoyxatKitoblarniKorsatadi() {
        let app = XCUIApplication()
        app.launch()

        // ContentView'dagi accessibilityIdentifier orqali qatorni topamiz
        let birinchiKitob = app.cells["book-1"]
        XCTAssertTrue(birinchiKitob.waitForExistence(timeout: 10))
        XCTAssertTrue(app.navigationBars["Kutubxona"].exists)
    }
}
# App/BUILD.bazel (davomi)
swift_library(
    name = "UITestsLib",
    testonly = True,
    srcs = glob(["UITests/**/*.swift"]),
)

ios_ui_test(
    name = "KutubxonaUITests",
    minimum_os_version = "17.0",
    test_host = ":Kutubxona",
    deps = [":UITestsLib"],
)

Ishga tushiramiz — va ikki marta muammoga duch kelamiz. Bu — haqiqiy, biz uchragan hikoya:

t = 4.14s Waiting 10.0s for "book-1" Cell to exist
t = 5.19s   Checking existence of `"book-1" Cell`
... (10 soniya kutadi, topolmaydi)
/App/UITests/KutubxonaUITests.swift:13: error: ... XCTAssertTrue failed
Test Case '...testRoyxatKitoblarniKorsatadi' failed (14.637 seconds).
...
-- Test timed out at ... UTC --
//App:KutubxonaUITests                                                  TIMEOUT in 300.2s

Ikkita alohida muammo yuz berdi:

  1. Test o'zi yiqildi (14.6 soniyada): app.cells["book-1"] hech qachon topilmadi. Sababi — SwiftUI List ichidagi view'ga qo'yilgan .accessibilityIdentifier o'sha identifikatorni cell (XCUIElement.cells) sifatida qidirsa mos kelmaydi. Identifikator ichki element'ga tegadi, cell'ning o'ziga emas.
  2. **Keyin xcodebuild osilib qoldi:** test 14.6 soniyada yiqilgandan so'ng, xcodebuild natijani yig'ish/tozalash bosqichida (bir nechta parallel test agentlari simulyatorlarni band qilgan og'ir yuk ostida) qariyb 4.5 daqiqa osilib qoldi. Oxirida Bazel'ning 300 soniyalik timeout'i uni majburan to'xtatdi: TIMEOUT in 300.2s.

Log'da yana shunaqa shovqin bor edi: IDESchemeActionSDKRecord ... invalidDigitCount(94232) — bu Xcode 27.1'ning build raqamini (24A94232) tahlil qilishdagi zararsiz nosozligi, testga aloqasi yo'q.

Tuzatish. 1-muammoni hal qilamiz: app.cells[...] o'rniga ishonchliroq so'rovlar ishlatamiz — navigatsiya paneli va ko'rinadigan matn (SwiftUI List'da staticTexts cell'dan ishonchliroq):

// App/UITests/KutubxonaUITests.swift — TUZATILGAN (o'tadi)
import XCTest

final class KutubxonaUITests: XCTestCase {
    func testRoyxatKitoblarniKorsatadi() {
        let app = XCUIApplication()
        app.launch()

        // Navigatsiya sarlavhasi "Kutubxona" — eng ishonchli element
        XCTAssertTrue(app.navigationBars["Kutubxona"].waitForExistence(timeout: 20))

        // Kitob sarlavhasi ro'yxatda ko'rinadi
        XCTAssertTrue(app.staticTexts["O'tkan kunlar"].exists)
    }
}
bazel test //App:KutubxonaUITests
//App:KutubxonaUITests                                                   PASSED in 26.5s
Executed 1 out of 1 test: 1 test passes.

Endi o'tdi — PASSED in 26.5s.

⚠️ Diqqat: Bu hikoya UI testlarning haqiqatini ko'rsatadi. Ular (1) sekin — bizda PASSED in 26.5s, logic testdan ~15 barobar sekin; (2) mo'rt (flaky) — noto'g'ri so'rov yoki og'ir yuk ularni yiqitadi; (3) timeout xavfi bor — yiqilgan UI test ba'zan toza tugamay, Bazel timeout'igacha osilishi mumkin. Shuning uchun UI testlarni kam yozing (faqat eng muhim foydalanuvchi oqimlari uchun) va ularga mos size/timeout bering (masalan size = "large", 900 s).

Uch xil testni yonma-yon qo'yaylik:

Uch xil test — logic (1.8s, test_host yo'q), hosted (17.7s, test_host=ilova), UI (15s+, XCUIApplication) — tezligi va qachon ishlatilishi

Bu — 4-darsdagi QA kursida ko'rgan test piramidasining Bazel'dagi aksi: ko'p logic test (tez, arzon, pastki qatlam), kamroq hosted test, va juda oz UI test (sekin, mo'rt, yuqori qatlam). Bazel har uchalasining natijasini keshlaydi — lekin yozish arzonrog'i har doim g'olib.


11. Coverage — kod qamrovi {#coverage}

Code coverage (kod qamrovi) — testlaringiz kodning qaysi qatorlarini haqiqatan ishga tushirganini o'lchaydi. 90% qamrov degani: kodning 90% qatorlari kamida bitta test tomonidan bajarilgan. Bu — testlaringizdagi "ko'r nuqtalar"ni topish usuli (hech qachon ishlamaydigan else shoxlari, sinalmagan xato yo'llari).

Qanday ishlaydi

Bazel'da coverage buyrug'i bazel coverage (xuddi bazel test kabi, lekin instrumentatsiya yoqilgan holda). Mexanizm (Bazel hujjatlaridan):

  1. Bazel kodni instrumentatsiya qiladi (--instrumentation_filter qaysi fayllar qamrovga kirishini belgilaydi; standart holda Bazel target package'ini tanlaydi va buni INFO xabarida chop etadi).
  2. Testlar ishga tushadi va bajarilgan qatorlar haqida ma'lumot yig'iladi.
  3. Hammasi bitta lcov fayliga birlashtiriladi: $(bazel info output_path)/_coverage/_coverage_report.dat.

Toza logikani (KutubxonaKit) coverage qilishning eng oson yo'li — uni macOS swift_test sifatida ham e'lon qilish. Bu bonus: swift_test XCTest va Swift Testing'ni simulyatorsiz, to'g'ridan-to'g'ri macOS'da ishga tushiradi — tez va coverage uchun qulay:

# KutubxonaKit/BUILD.bazel (davomi)
load("@rules_swift//swift:swift.bzl", "swift_test")

swift_test(
    name = "KutubxonaKitSwiftTest",
    srcs = glob(["Tests/**/*.swift"]),
    deps = [":KutubxonaKit"],
)

bazel test //KutubxonaKit:KutubxonaKitSwiftTest → PASSED in 1.0s (simulyatordagi 1.8 s'dan ham tez, chunki simulyator kerak emas). Endi shu target'dan coverage olamiz:

bazel coverage //KutubxonaKit:KutubxonaKitSwiftTest --combined_report=lcov

lcov — bu matnli format: har fayl uchun SF: (source file), DA: (qator raqami, necha marta bajarilgani), LH:/LF: (qatorlar: hit/found). Uni odam o'qiydigan HTML hisobotga aylantirish uchun genhtml ishlatiladi (u lcov paketida; brew install lcov):

genhtml --output genhtml "$(bazel info output_path)/_coverage/_coverage_report.dat"
# keyin genhtml/index.html ni brauzerda oching

Rule darajasida ikkita foydali atribut bor (rules_apple ios_unit_testdan):

  • collect_code_coverage — shu target uchun qamrov yig'ish (--collect_code_coverage=yes bilan).
  • test_coverage_manifest — qamrov qaysi fayllar bilan cheklanishini belgilaydigan fayl (katta loyihada instrumentatsiyani faqat o'z kodingiz bilan cheklash uchun).

Ochig'ini aytamiz: bu toolchain'da coverage'ning cheklovi

STYLE talabiga sodiq qolib, haqiqatni aytamiz: biz Bazel 9.3.0 + rules_apple 5.3.0 + rules_swift 4.2.1 + Xcode 27.1 (clang 21) muhitida coverage'ni sinab ko'rdik va ikki aniq muammoga duch keldik.

**1. iOS simulyator testida (ios_unit_test) coverage qurilmaydi.** bazel coverage //KutubxonaKit:KutubxonaKitTests quyidagi linker xatosini beradi:

Undefined symbols for architecture arm64:
  "___llvm_profile_runtime", referenced from:
      ___llvm_profile_runtime_user in libKutubxonaKitTestsLib.a[2](...)
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1

Ya'ni instrumentatsiya qilingan kod LLVM profil runtime'iga (__llvm_profile_runtime) ishora qiladi, lekin bu runtime (libclang_rt.profile_iossim.a, u toolchain'da mavjud) test binary'siga ulanmaydi. Bu — rules_swift'ning shu versiyasi + Xcode 27.1 ning o'zaro muammosi (yangi clang 21 yo'llari bilan).

**2. macOS swift_testda coverage quriladi va ishlaydi, lekin qator ma'lumoti bo'sh.** Yuqoridagi swift_test uchun bazel coverage muvaffaqiyatli o'tadi va lcov hisobotini yaratadi — fayllar ro'yxati to'g'ri:

SF:KutubxonaKit/Sources/Book.swift
FNF:0
FNH:0
LH:0
LF:0
SF:KutubxonaKit/Sources/BookLibrary.swift
FNF:0
FNH:0
LH:0
LF:0

...

lekin har fayl uchun LF:0/LH:0 (qatorlar topilmadi, bajarilmadi) — ya'ni Swift qamrov ma'lumoti lcov'ga o'girilmadi (instrumentatsiya swiftc bilan, lcov konvertatsiyasi esa llvm-cov bilan; versiyalar mos kelmasligi odatiy sabab).

⚠️ Diqqat: Demak, bazel coverage buyrug'i ishlaydi (mexanizm to'g'ri), lekin bu aniq versiyalar to'plamida (2026-oktabr) Swift iOS testlari uchun foydali qator qamrovi bermadi. Bu — Bazel + iOS ekotizimidagi ma'lum "qirrali joy". Amaliyotda jamoalar coverage'ni ko'pincha rules_xcodeproj orqali (Xcode'ning o'z coverage mexanizmi bilan) yoki xcodebuild'da oladi, yoki Xcode/llvm versiyalarini qat'iy qadab (pinning) muammoni hal qiladi. O'z toolchain'ingizda bazel coverageni sinab ko'ring — versiyalaringizda ishlashi mumkin. (Bu cheklov bizning hisobotimizda ham qayd etilgan.)

💡 Pro tip: Coverage raqami maqsad emas, vosita. 100% qamrov — kod xatosiz degani emas (sifatsiz tekshiruvlar ham qatorni "qoplaydi"). Qamrovni "qaysi muhim yo'llar umuman sinalmagan?" degan savolga javob sifatida ishlating, KPI sifatida emas.


12. ios_build_test — "shunchaki quriladimi?" {#build-test}

Ba'zan sizda test yozilmagan kutubxona bo'ladi, lekin uning iOS uchun kompilyatsiya bo'lishini kafolatlamoqchisiz. Masalan, faqat macOS'da ishlab turgan bo'lsangiz, lekin kod iOS uchun ham qurilishini CI'da tekshirmoqchisiz. Buning uchun ios_build_test bor: u kod ishini sinamaydi, faqat ko'rsatilgan target'lar quriladimi yoki yo'qligini test qiladi.

# KutubxonaKit/BUILD.bazel (davomi)
load("@rules_apple//apple:ios.bzl", "ios_build_test")

ios_build_test(
    name = "KutubxonaKitBuildTest",
    minimum_os_version = "17.0",
    targets = [":KutubxonaKit"],
)

bazel test //KutubxonaKit:KutubxonaKitBuildTest — agar KutubxonaKit iOS uchun qurilsa, "PASSED"; qurilmasa, "FAILED". Simulyator kerak emas, test kodi kerak emas. Bu — eng arzon "ishonchlilik seti": kutubxona kompilyatsiyadan o'tishi kafolatlanadi.

💡 Pro tip: ios_build_test ayniqsa faqat bir platformada ishlab, lekin bir nechta platforma uchun qo'llab-quvvatlanadigan kod yozganda foydali. bazel test //... butun repoda build_test'larni ham ishga tushiradi — ya'ni bitta buyruq bilan "hamma narsa hamma platformada quriladimi va testlar o'tadimi?" degan savolga javob olasiz.


13. Katta jamoalarda test strategiyasi {#strategiya}

Hozirgacha o'rgangan narsalar — test keshi, logic/hosted/UI testlar, flaky boshqaruvi — katta jamoalarda nega muhimligini raqamlar bilan ko'raylik. (Diqqat: quyidagi raqamlarning bir qismi vendor e'lon qilgan case study'lardan; biz ularni shunday belgilaymiz.)

🏢 Real-world — Spotify (Bazel). InfoQ'ning 2023-yil oktabrdagi hisobotiga ko'ra, Spotify iOS'da Bazel'ga (remote cache bilan) o'tgach, birlashtirishdan oldingi (p75 pre-merge) kutish vaqti 2022-yil mayidan keyin "to'rt barobar" qisqargan; eng og'ir CI konfiguratsiyasi 45+ daqiqadan 10 daqiqadan kamga tushgan. Buning asosi — biz shu darsda ko'rgan narsa: keshlangan natijalar va faqat o'zgarganini qayta ishlaydigan model.

🏢 Real-world — Monzo (Tuist, vendor case study). Tuist 2026-yil 26-mayda e'lon qilgan Monzo case study'siga ko'ra, Monzo'da ~200 modul, 45+ iOS muhandisi, 1.9M qator Swift, 20 ming unit test va 8 ming snapshot test bor. CI PR'lari 70 daqiqadan oshib ketgan edi; modul keshini har 30 daqiqada main'da isitib turish bilan P50 PR vaqti 52 → 15 daqiqaga (−71%) tushgan. 20 mingta testni har safar noldan ishga tushirmaslik — bu Bazel test keshi hal qiladigan aynan o'sha muammo.

🏢 Real-world — Trendyol (Tuist, vendor case study). Trendyol'ning 2024-yil 16-dekabrdagi mijoz hikoyasiga ko'ra, selective (tanlab) unit testlar fikr-mulohaza (feedback) siklini 80% gacha tezlashtirgan, va UI testlar uchun toza muhit tayyorlash 15+ daqiqadan ~30 soniyagacha qisqargan.

Bu hikoyalarning umumiy g'oyasi bitta: o'zgarmagan testni qayta ishga tushirma. Tuist buni "selective testing" deb ataydi (target hash o'zgarganini test qiladi; target darajasida). Bazel buni ikki mexanizm bilan qiladi:

  1. Test natijasi keshi (shu darsda ko'rdik): o'zgarmagan testlar (cached) PASSED.
  2. Faqat ta'sirlangan target'larni test qilish: git diff → o'zgargan fayllar → ularga bog'liq test target'larini bazel query 'rdeps(//..., set(...))' bilan topish va faqat ularni ishga tushirish. Buni 11- va 16-darslarda to'liq quramiz.

🧠 Esda tuting: Bazel test keshi "bepul" selective testing beradi — bazel test //... har doim o'zgarmaganlarini keshdan oladi. Lekin eng katta tejash CI'da remote cache bilan keladi: bitta muhandis (yoki main build'i) test natijasini remote cache'ga yozadi, qolganlar esa uni yuklab oladi va umuman qayta ishlamaydi. Bu — 10- va 11-darslarning mavzusi.

⚠️ Diqqat: Yuqoridagi Monzo va Trendyol raqamlari Tuist blogida e'lon qilingan mijoz hikoyalaridan (vendor manbasi), mustaqil o'lchov emas. Spotify raqami InfoQ hisoboti orqali. Bazel vs Tuist vs Xcode uchun neytral, mustaqil benchmark mavjud emas — har bir raqam o'sha kompaniyaning avvalgi holatiga nisbatan. Bu haqda 20-darsda batafsil to'xtalamiz.


Amaliy mashqlar {#mashqlar}

  1. Yangi metod + test. BookLibrary'ga averagePages: Int (o'rtacha sahifa soni, kitoblar bo'sh bo'lsa 0) qo'shing. Unga ham XCTest, ham Swift Testing testi yozing. bazel test'da ikkala framework summary'si ham chiqishini tekshiring.
  2. Filtrlash. --test_filter bilan faqat testSortedByYear metodini ishga tushiring. Keyin regular expression bilan testSearch* va testSort* ni birga ishga tushirishga harakat qiling.
  3. size to'g'rilash. KutubxonaKitTestsga size = "small" qo'shing va bazel test //KutubxonaKit:KutubxonaKitTests --test_verbose_timeout_warnings bilan "size is too big" ogohlantirishi yo'qolganini tasdiqlang.
  4. ⭐ Flaky testni ushlash. Bir testda #expect(Bool.random()) yoki shunga o'xshash beqaror shart yozing. --runs_per_test=20 bilan ishga tushiring va ba'zi urinishlar yiqilishini ko'ring. Keyin target'ga flaky = True qo'shib, xatti-harakat qanday o'zgarishini kuzating. (Tajribadan so'ng beqaror testni o'chiring — flaky testlar repoda qolmasin.)
  5. ⭐ Tasodifiy tartib. KutubxonaKitTests runner'ini ios_xctestrun_random_runnerga o'zgartiring va testlar tasodifiy tartibda o'tishini tekshiring. Testlaringiz bir-biriga bog'liq emasligiga ishonch hosil qiling.
  6. ⭐ UI testni kengaytirish. KutubxonaUITestsga ikkinchi tekshiruv qo'shing: ro'yxatda uchala kitob ham ko'rinishini (app.staticTexts bilan) tasdiqlang. UI test sizeini "large" qiling va nega buni tanlaganingizni izohlang.

O'zingizni tekshiring {#checklist}

  • [ ] Test kodini nega alohida swift_library (testonly = True) ga ajratish kerakligini tushuntira olaman.
  • [ ] Bitta ios_unit_testda XCTest va Swift Testing'ni birga ishlata olaman.
  • [ ] bazel test, --test_output=errors|all, --test_filter ni kundalik ishda qo'llayman.
  • [ ] (cached) PASSED qachon chiqishini va nima uni buzishini aytib bera olaman.
  • [ ] size/timeout ning ma'nosini va standart qiymatlarini bilaman (small → 60 s).
  • [ ] --runs_per_test, flaky = True, shard_count qachon kerakligini bilaman.
  • [ ] Logic test, hosted test (test_host) va UI test (ios_ui_test) farqini va tezlik farqini tushunaman.
  • [ ] Xcode 27'da simulyatorni .bazelrc orqali to'g'ri tanlay olaman.

Xulosa {#xulosa}

  • Bazel'da test strukturasi doim uch qismli: asosiy swift_library → testonly test swift_library → ios_unit_test.
  • XCTest ham, Swift Testing ham bitta target'da ishlaydi — rules_swift ikkalasini tug'ma qo'llaydi.
  • Bazel'ning asosiy super-kuchi — test natijasi keshi: o'zgarmagan test (cached) PASSED beradi va simulyatorda umuman ishlamaydi (bizda 1.8 s o'rniga 0.19 s).
  • size/timeout/flaky/shard_count — testlar sifatini va tezligini boshqarish vositalari; eng kichik mos sizeni tanlang.
  • Uch xil test uch xil tezlik: logic (~1.8 s), hosted (~17.7 s), UI (~26.5 s) — shuning uchun ko'p logic, oz UI test yozing.
  • UI testlar mo'rt va timeout'ga moyil — biz buni haqiqiy hikoyada ko'rdik (noto'g'ri so'rov + xcodebuild osilishi → TIMEOUT in 300.2s).
  • Simulyatorni .bazelrc'da build sozlamasi orqali tanlang — Xcode 27'dagi "iPhone 15" tuzog'idan shunday qutulasiz.
  • Coverage toza logika uchun swift_test orqali oson olinadi; iOS simulyator testlari uchun bu toolchain'da hozircha cheklov bor (quyida ko'rdik).

Keyingi dars: Bazel 7: Tashqi dependency'lar — Bzlmod, BCR, SPM paketlari va xcframework'lar — endi loyihamizga tashqi kutubxonalarni (SnapKit, swift-collections kabi) Bazel orqali qanday qo'shishni o'rganamiz.


Manbalar {#manbalar}

  1. rules_apple — rules-ios.md (ios_unit_test, ios_ui_test, ios_build_test, ios_xctestrun_runner)
  2. rules_apple — common_info.md (test runfiles, Xcode versiya tanlash)
  3. rules_apple releases (5.0.0 — standart test runner xctestrun'ga o'tdi)
  4. rules_swift — swift_test, XCTest discovery, test filtering
  5. Bazel — Code coverage (lcov, genhtml, instrumentation)
  6. Bazel — Test encyclopedia (size, timeout, sharding)
  7. Swift Testing (Apple hujjatlari)
  8. Spotify iOS Bazel'ga o'tishi (InfoQ, 2023-oktabr)
  9. Monzo case study (Tuist, 2026-05-26 — vendor)
  10. Trendyol case study (Tuist, 2024-12-16 — vendor)

Markdown manba

iOS Lessons