CodeGym /Kurslar /Swift SELF /XCTest strukturu: XCTestCase, setUp/tearDown

XCTest strukturu: XCTestCase, setUp/tearDown

Swift SELF
Səviyyə, Dərs
Mövcuddur

1. unit testlər və SwiftPM-də XCTest necə qurulub

İnsanlar ilk dəfə «unit testlər» sözünü eşidəndə çox vaxt bunu «robotlar üçün əlavə ev tapşırığı» kimi təsəvvür edirlər. Praktikada bu daha çox təhlükəsizlik kəmərinə bənzəyir: yavaş sürəndə artıq görünür, amma bir gün həqiqətən xilas edir — həm günü, həm də əsəbləri. Amma ağıllı yoxlamalar yazmazdan əvvəl «sürət qutusu»nu anlamaq lazımdır: testlər harada yaşayır, necə işə düşür, onları kim tapır və niyə setUp()/tearDown() dekorativ metodlar yox, təmizlik üçün vacib ritualdır.

Unit test — kodun kiçik bir hissəsinin davranışını avtomatik yoxlamaqdır (funksiya, metod, kiçik tip). Burada «kiçik» əsas sözdür: biz bütün dünyanı test etmirik, biz qaydanı test edirik. Məsələn: «komandanın normallaşdırılması boşluqları kəsir və onu kiçik registrə çevirir».

XCTest testləri harada yaşayır: Tests/ qovluğu və test target

Əgər Sources/ məhsulun hazırlandığı «mətbəx»dirsə, Tests/ isə ağ əlcəklə gəlib belə deyən «sanitar nəzarət»dir: “Aha, göstərın bunu necə hazırlamısınız”. Swift Package Manager-də testlər həqiqətən ayrıca yaşayır: bu ayrıca build məqsədidir (test target), və bu vacibdir, çünki testlər ayrıca kompilyasiya olunur və kodunuzu modul kimi import edir.

SwiftPM-də kifayət qədər sərt və faydalı bir razılaşma var: test target-lar yuxarı səviyyəli Tests/ qovluğu altında yerləşir və test target adının sonunda adətən Tests olur. SwiftPM məhz belə razılaşmalara əsaslanır ki, «bu testdir, ya yox»u anlaya bilsin və düzgün build modelini qura bilsin.

Paketin tipik strukturu üçün nümunə (sadələşdirilmiş):

LibraryCLI/
├─ Package.swift
├─ Sources/
│  └─ LibraryCLI/
│     └─ main.swift
└─ Tests/
   └─ LibraryCLITests/
      └─ SmokeTests.swift

Terminalda testləri işə salmaq mümkün qədər dostcasına görünür (IT-də nadir haldır):

swift test

Və burada vacib fikir budur: testlər də eyni Swift kodudur, sadəcə xüsusi runner tərəfindən işə salınır.

XCTestCase: testlər üçün «konteyner» və niyə klass kimi görünür

İlk dəfə final class SomethingTests: XCTestCase görəndə belə bir sual yarana bilər: «Niyə class? Axı biz struct-u sevirik!» Sevirik, bəli. Amma XCTest tarixən obyekt modelinə əsaslanır və test dəsti həyat dövrü olan bir obyektdir: yarat, hazırla, testi icra et, sonra təmizlə. Bunun üçün class təbii seçimdir.

Bundan əlavə, Apple platformalarında XCTest tarixən Objective-C runtime mexanizmlərindən istifadə edir ki, XCTestCase-dən törəyənlər arasında testləri tapsın (klass və metod metadatasına görə). Digər platformalarda və SwiftPM ssenarisində testlər adətən SwiftPM-in öz məntiqi ilə aşkar olunur (o, test metodlarını «indeksləyir» və test siyahısını runner-ə ötürür). İndi bu daxili detalları yadda saxlamağınıza ehtiyac yoxdur, amma bunu anlamaq faydalıdır: XCTest həqiqətən testləri axtarır, təsadüfi nəyisə işə salmır.

Minimal test class belə görünür.


import XCTest

final class SmokeTests: XCTestCase {
    func testExample() {
        XCTAssertTrue(true) // test, which always passes (sometimes this is useful)
    }
}

Burada əsas məqam artıq görünür:

  • import XCTest — framework qoşuldu.
  • final class …: XCTestCase — test class elan edildi.
  • func test…() — test metodu.

XCTest testləri necə tapır: niyə metod test ilə başlamalıdır

XCTest-də sadə, amma dəmir kimi möhkəm bir razılaşma var: test — instance metodu olmalıdır, klass XCTestCase-dən olmalıdır, adı test ilə başlamalıdır və «adi» signature-ə malik olmalıdır (parameter olmadan, qaytarılan dəyər — Void). Praktikada bu o deməkdir: metodun test kimi işə düşməsini istəyirsinizsə, onu test... adlandırın.

Bu, kapriz deyil. XCTest elə qurulub ki, testləri əl ilə siyahıdan yox, razılaşma əsasında avtomatik tapsın. Apple platformalarında bu metadata və runtime introspection ilə, SwiftPM-də isə paket daxilində testlərin aşkar edilməsi mexanizmi ilə bağlıdır.

Sadə nümunəyə baxaq.

import XCTest

final class DiscoveryTests: XCTestCase {
    func helperNotATest() {
        // Bu metod test kimi işə düşməyəcək: ad "test" ilə başlamır
    }

    func testRuns() {
        XCTAssertTrue(true) // Bu işə düşəcək
    }
}

Praktik nəticə: əgər yazdığınız test «işə düşmürsə», ilk yoxlanacaq şey — metodun adı. Bu, hətta təcrübəli developer-lərdə də tipik haldır: beyin məntiqlə məşğul olur, barmaqlar isə metodu adi koddakı kimi adlandırır.

2. Həyat dövrü: setUp() və tearDown()

setUp()/tearDown() ilə yeni başlayanları ən çox eyni tələ gözləyir: elə gəlir ki, setUp() «bütün testlərdən əvvəl bir dəfə» çağırılır. Amma yox. XCTest-də əsas model belədir: hər test təmiz vəziyyətdən başlamalıdır, buna görə setUp() hər test metodundan əvvəl, tearDown() isə hər testdən sonra icra olunur.

Yəni real ardıcıllıq təxminən belədir:

setUp → testA → tearDown → setUp → testB → tearDown → ...

Bu, qəsdən belə edilib ki, testlər işə düşmə sırasından asılı olmasın, bir-birinin vəziyyətini «yeməsin» və seriala çevrilməsin: «əvvəlki seriyada...».

Bunu kiçik sxem şəklində təsvir edək.

sequenceDiagram
    participant R as XCTest Runner
    participant T as YourTests (XCTestCase instance)

    R->>T: setUp()
    R->>T: testFirst()
    R->>T: tearDown()

    R->>T: setUp()
    R->>T: testSecond()
    R->>T: tearDown()

Yadda saxlamaq üçün daha bir qısa cədvəl:

Metod Nə vaxt çağırılır Nə üçün
setUp()
hər test...()-dən əvvəl «təmiz start» yaratmaq: SUT, data, mühit
tearDown()
hər test...()-dən sonra resursları təmizləmək, istinadları sıfırlamaq, zibil qoymamaq

3. SUT və hazırlığın təşkili

SUT nədir və sut harada saxlanmalıdır

Testlərdə tez-tez SUT termini istifadə olunur — System Under Test, yəni «test etdiyimiz obyekt». Bəzən bu funksiya olacaq, bəzən tip (struct/class), bəzən servis. Testlərin daha rahat oxunması üçün SUT çox vaxt sut adlandırılır (bəli, bu “sut” kimi görünür... və bəli, mən də ilk dəfə bunu kompilyatorun import unutduğunuzda çıxardığı səs kimi oxumuşdum).

Məntiq adətən belədir: əgər SUT hər test üçün eyni şəkildə yaradılırsa, onu setUp()-da yaratmaq rahatdır. Əgər fərqli testlər üçün fərqli konfiqurasiya lazımdırsa, SUT-u birbaşa testin içində yaratmaq daha yaxşıdır ki, giriş məlumatları gözlə görünən olsun və setUp()-ın «sehrində» gizlənməsin.

Sadə nümunədən başlayaq: bizdə kiçik bir Counter tipi var.

struct Counter {
    private(set) var value: Int = 0

    mutating func inc() {
        value += 1
    }
}

İndi sut-un setUp()-da yaradıldığı testlər.

import XCTest

final class CounterTests: XCTestCase {
    private var sut: Counter!

    override func setUp() {
        super.setUp()
        sut = Counter()
    }

    override func tearDown() {
        sut = nil
        super.tearDown()
    }

    func testInc_incrementsValue() {
        sut.inc()
        XCTAssertEqual(sut.value, 1)
    }
}

Burada bir neçə vacib şeyi eyni anda etdik, onları sözlə də deyək:

  • sut-u Counter! kimi elan etdik. Bu, «implicitly unwrapped optional»dir və burada uyğundur, çünki biz kontraktı təmin edirik: sut hər hansı test başlamazdan əvvəl setUp()-da ilkinləşdiriləcək. Əgər kontrakt pozularsa — testlər tez və səs-küylü şəkildə çökəcək, və bu normaldır.
  • super.setUp() və super.tearDown() çağırırıq. Bu, yaxşı vərdişdir: baza class da öz daxili hazırlığını/təmizləməsini edə bilər.
  • tearDown()-da sut-u = nil edirik. Value type-lar üçün bu kritik deyil, amma bu intizamdır: kursun davamında reference type-lar və resurslar olacaq, orada «istinadları sıfırlamaq» faydalı refleksdir.

Niyə setUp() «200 sətirlik ssenariyə» çevrilməməlidir

setUp()-a öyrəşəndə içərisinə hər şeyi qoymaq istəyirsən: SUT yaratmaq, test məlumatları yaratmaq, 15 obyekt qurmaq, 3 massiv hazırlamaq, fixture yükləmək... Və məhz burada sakit faciə başlayır.

Problem bunda deyil ki, bunu etmək olmaz. Problem odur ki, test oxunmaz olur. testSomething() açırsınız və yalnız «Act» və «Assert» görürsünüz, «Arrange» isə setUp()-ın içində və daha üç helper metodunda gizlənib. Nəticədə test çökür, siz isə detektiv oyunu oynayırsınız: «Bəs giriş məlumatları bizdə necə idi?»

Ona görə başlayanlar üçün əsas qayda belədir: setUp() yalnız demək olar ki, bütün testlər üçün eyni olan və həqiqətən «fon hazırlığı» sayılan işi görməlidir. Konkret ssenariyə aid olan hər şeyi birbaşa testin içində yazmaq daha yaxşıdır ki, kontekst göz önündə olsun.

CLI üçün komandanın normallaşdırılmasında iki yanaşmanı müqayisə edək.

import Foundation

func normalizeCommand(_ input: String) -> String {
    input.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}

Əgər SUT sadəcə funksiyadırsa, setUp() ümumiyyətlə lazım deyil, çünki «hazırlanacaq» heç nə yoxdur. Test maksimum şəffaf olacaq.

import XCTest

final class NormalizeCommandTests: XCTestCase {
    func testNormalizeCommand_trimsAndLowercases() {
        let result = normalizeCommand("  ADD  ")
        XCTAssertEqual(result, "add")
    }
}

Burada fayl arasında hoppanmadan hər şey görünür: giriş " ADD ", gözlənti "add".

Nümunə: LibraryCLI-də ilk test

İndi tədris layihəmizə enək. Arqumentləri və memarlığı ağırlaşdırmayacağıq, hansı target-i test etməyi müzakirə etməyəcəyik — bu dərsin ideyası məhz XCTestCase mexanikası və həyat dövrüdür. Təsəvvür edək ki, Sources/LibraryCLI/-də CommandNormalizer.swift adlı fayl var və orada normallaşdırma funksiyası yerləşir.

import Foundation

func normalizeCommand(_ input: String) -> String {
    input.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}

Sonra test faylı yaradırıq, məsələn Tests/LibraryCLITests/NormalizeCommandTests.swift.

import XCTest
@testable import LibraryCLI

final class NormalizeCommandTests: XCTestCase {
    func testNormalizeCommand_whenSpacesAndUppercase_returnsLowercasedTrimmed() {
        let result = normalizeCommand("  ADD  ")
        XCTAssertEqual(result, "add")
    }
}

Burada @testable import ortaya çıxır. Bu gün modul sərhədlərinə dərindən girmirik (bu ayrıca dərsdə olacaq), amma minimal olaraq bunu başa düşmək vacibdir: testlər production kodunu modul kimi import edir, çünki onlar ayrıca test target-də yaşayır. @testable testlərə import edilən modulun internal simvollarını görməyə imkan verir, amma private və fileprivate deyil.

İndi isə işə salaq:

swift test

Əgər test çökərsə, swift test hansı testin və harada çökdüyünü göstərəcək. Bu sehr deyil — sadəcə intizamdır: testlər proqnozlaşdırıla bilən adlarla adlanır, avtomatik işə düşür və hər biri təmiz vəziyyətdən başlayır.

Daha bir setUp()/tearDown() nümunəsi: testlər vəziyyəti bölüşmür

setUp()/tearDown()-ı «hiss etmək» üçün ən sürətli yol iki test yazmaqdır: biri vəziyyəti dəyişir, digəri isə vəziyyətin yenə təmiz olduğunu yoxlayır. Bu, bir az hosteldəki qonşunuzun həqiqətən qəhvə içəndən sonra fincanı yuyub-yumadığını yoxlamağa bənzəyir (spoiler: adətən yox, amma XCTest — yuyur).

import XCTest

final class CounterIsolationTests: XCTestCase {
    private var sut: Counter!

    override func setUp() {
        super.setUp()
        sut = Counter()
    }

    override func tearDown() {
        sut = nil
        super.tearDown()
    }

    func testInc_changesValue() {
        sut.inc()
        XCTAssertEqual(sut.value, 1)
    }

    func testStartsFromZero_eachTime() {
        XCTAssertEqual(sut.value, 0)
    }
}

Əgər setUp() «bütün testlər üçün bir dəfə» çağırılsaydı, ikinci test value == 1 görə bilərdi (əgər birinci əvvəl icra olunsaydı). Amma XCTest-də hər test öz setUp()/tearDown()-ı ilə əhatələnir, buna görə ikinci test sabit şəkildə sıfır görməlidir.

4. XCTestCase və setUp()/tearDown() ilə işləyərkən tipik səhvlər

Səhv №1: metod test ilə başlamadığı üçün test «işə düşmür».
Bu ən geniş yayılmış hekayədir. Siz func checkParse() yazırsınız və bunun test olduğuna əminsiniz. XCTest belə düşünmür. Razılaşma test aşkar etmə mexanizminin bir hissəsidir. Ona əməl etməsəniz, metod sadəcə adi metod olacaq və heç vaxt test kimi icra olunmayacaq.

Səhv №2: setUp() -ın klass üçün bir dəfə çağırıldığını düşünmək.
Buna görə insanlar setUp()-a «yığılan vəziyyət» qoyur və niyə testlərin bəzən keçib, bəzən keçmədiyinə təəccüblənirlər. Əslində setUp() və tearDown() hər testi əhatə edir. Bu, testlərin sıra baxımından müstəqil olması və vəziyyəti bölüşməməsi üçün edilir.

Səhv №3: testlər arasında yaşayan ortaq sut.
Əgər static var sut etmisinizsə və ya SUT-u haradasa qlobal şəkildə yaratmısınızsa, əslində izolyasiya ideyasını ləğv etmisiniz. Belə test dəsti «sızmağa» başlayır: bir test vəziyyəti hazırlayır, digəri təsadüfən ondan istifadə edir, üçüncüsü isə ayrıca işə salınanda çökür. Normalda SUT hər test üçün yenidən yaradılır — ya setUp()-da, ya da birbaşa testin içində.

Səhv №4: setUp() böyük gizli ssenariyə çevrilib.
Formal olaraq testlər keçir, amma çökən testi oxumaq axtarışa çevrilir: giriş məlumatını anlamaq üçün əvvəl setUp()-a, sonra helper-ə, sonra daha bir helper-ə baxmaq lazımdır. Daha yaxşısı setUp()-u minimal saxlamaqdır: «SUT yaratmaq», «əsas konfiqurasiyanı hazırlamaq», ssenari məlumatını isə testin özündə yazmaq.

Səhv №5: super.setUp() / super.tearDown() çağırmağı unutmaq.
Çox vaxt onsuz da «işləyir», amma bu pis vərdişdir. XCTestCase öz məntiqi olan baza class-dır. super çağırışı framework mexanikasına hörmətdir və nadir, amma narahatedici sürprizlərə qarşı sığortadır.

1
Tapşırıq
Swift SELF, səviyyə, dərs
Bağlanıb
Təmiz sayğac
Təmiz sayğac
1
Tapşırıq
Swift SELF, səviyyə, dərs
Bağlanıb
Todo siyahısı testləri
Todo siyahısı testləri
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION