تطوير الويب

اختبار app SwiftUI بـ Swift Testing وXcode Previews

6 دقائق للقراءة

📘 الدليل الرئيسي: تطوير تطبيق iOS بـ Swift وSwiftUI: بانوراما 2026. هذا الدليل يفترض إتقان أساسيات SwiftUI؛ لعميل الشبكة المُختبَر هنا، راجع استهلاك API REST بـ async/await.

اختبار الكود نادرًا ما يكون الأولوية الأولى لمبتدئ Swift، رغم أنّ الاختبارات تعرض حلقة تغذية راجعة أقصر بكثير من دورة build-run-clic في simulator. قدّمت Apple Swift Testing في 2024 مع Swift 6.0، framework حديث يحلّ تدريجيًّا محلّ XCTest للاختبارات الوحدوية. بالموازاة، Xcode Previews تُتيح فحص عرض view SwiftUI دون تشغيلها في simulator. مقترنَين، تُقَصِّر هاتان الأداتان دورة التكرار جذريًّا. يُفَصِّل هذا الدليل الاثنتين خطوة بخطوة مع مشروع مثال، view model observable، وعميل شبكة نختبره من كلّ جوانبه.

المتطلّبات

  • Xcode 26 على macOS Tahoe.
  • هدف iOS 17 كحدّ أدنى (Swift Testing + Xcode Previews الحديثة).
  • أساسيات Swift، مفاهيم structs وclosures.
  • الوقت المتوقّع: 80 إلى 120 دقيقة.
  • المستوى: متوسّط.

الخطوة 1 — إضافة هدف اختبارات لمشروع قائم

إن أُنشئ المشروع دون تعليم Include Tests، نُضيف هدف اختبارات لاحقًا. الإجراء آليّ ولا يتطلّب إعدادًا يدويًّا مُعَقَّدًا.

في نافذة Xcode، انقر اسم المشروع (أيقونة جذر زرقاء)، ثم زرّ + أسفل عمود Targets. اختر Unit Testing Bundle، انقر Next، اقبل الاسم المُقتَرَح (MonAppTests)، اربط بالهدف الرئيسي. Xcode يُولِّد ملفّ MonAppTests.swift بهيكل XCTest. يمكن حذفه وإنشاء ملفّ SwiftTestingDemoTests.swift مكانه.

import Testing
@testable import MonApp

@Suite("Suite de démarrage")
struct DemarrageTests {
    @Test func sanityCheck() async throws {
        #expect(1 + 1 == 2)
    }
}

@testable import يفتح وصول رموز internal للوحدة المُختبَرة. الـ macro @Suite تُجَمِّع الاختبارات حسب الموضوع؛ @Test يَسِم دالّة اختبار؛ #expect هي macro التأكيد. تشغيل الاختبار بـ ⌘U يجب عرض علامة خضراء.

الخطوة 2 — كتابة اختبار لـ view model @Observable

view model مَوسوم @Observable هو class عادية. نختبره بإنشائه في اختبار، استدعاء دوالّه، والتحقّق من خصائصه. لا حاجة لـ harness SwiftUI.

@Observable
final class CompteurVM {
    var valeur: Int = 0
    var maximum: Int = 10

    func incrementer() {
        if valeur < maximum { valeur += 1 }
    }

    func reinitialiser() { valeur = 0 }
}

@Suite("CompteurVM")
struct CompteurVMTests {

    @Test func incrementerAugmenteLaValeur() {
        let vm = CompteurVM()
        vm.incrementer()
        #expect(vm.valeur == 1)
    }

    @Test func incrementerStagneAuMaximum() {
        let vm = CompteurVM()
        vm.maximum = 3
        for _ in 0..<5 { vm.incrementer() }
        #expect(vm.valeur == 3)
    }

    @Test func reinitialiserRamenAZero() {
        let vm = CompteurVM()
        vm.incrementer()
        vm.incrementer()
        vm.reinitialiser()
        #expect(vm.valeur == 0)
    }
}

كلّ اختبار مستقلّ: مثيل جديد من CompteurVM يُنشَأ لكلّ اختبار. #expect يُقَيِّم تعبيرًا boolean ويعرض قيم المتغيّرات المُتَورِّطة في حالة الإخفاق — أفضلية محسوسة على XCTAssertEqual.

الخطوة 3 — معلَمَة اختبار بـ @Test(arguments:)

@Test(arguments: [
    (0, 1),
    (5, 6),
    (9, 10),
    (10, 10)
])
func incrementationParametree(initial: Int, attendu: Int) {
    let vm = CompteurVM()
    vm.valeur = initial
    vm.incrementer()
    #expect(vm.valeur == attendu)
}

المُبَلِّغ يعرض أربع تنفيذات منفصلة، واحدة لكلّ tuple. إن أخفقت إحداها، نُحَدِّد فورًا أيّها. هذا النمط يحلّ محلّ الحلقات القديمة في XCTestCase.

الخطوة 4 — اختبار كود غير متزامن

الاختبارات التي تستهلك دوالّ async تُوسَم نفسها async. لا XCTestExpectation، لا waitForExpectations — الـ framework ينتظر ببساطة نهاية الدالّة.

final class ClientAPIMock {
    let data: Data
    var nbAppels = 0

    init(data: Data) { self.data = data }

    func fetchPosts() async throws -> [Post] {
        nbAppels += 1
        return try JSONDecoder().decode([Post].self, from: data)
    }
}

@Test func clientDecodeUnePostSimple() async throws {
    let json = """
    [{ "userId": 1, "id": 42, "title": "T", "body": "B" }]
    """.data(using: .utf8)!

    let client = ClientAPIMock(data: json)
    let posts = try await client.fetchPosts()

    #expect(posts.count == 1)
    #expect(posts.first?.id == 42)
    #expect(client.nbAppels == 1)
}

دالّة الاختبار يمكن أن تُلقي وتنشر الخطأ؛ الـ framework يُبَلِّغ الإخفاق تلقائيًّا. الـ triple guillemet في Swift يُتيح كتابة JSON متعدّد الأسطر قابل للقراءة في الكود.

الخطوة 5 — اختبار حالة خطأ

enum CalculError: Error, Equatable {
    case divisionParZero
}

func diviser(_ a: Int, par b: Int) throws -> Int {
    guard b != 0 else { throw CalculError.divisionParZero }
    return a / b
}

@Test func divisionParZeroLeveLErreur() {
    #expect(throws: CalculError.divisionParZero) {
        try diviser(10, par: 0)
    }
}

@Test func divisionNormaleReussit() throws {
    let r = try diviser(10, par: 2)
    #expect(r == 5)
}

الـ macro #expect(throws:) تُنَفِّذ الـ closure، تلتقط الخطأ إن رُفع، وتتحقّق من تطابقه مع المنتظر. إن لم تُلقِ الـ closure شيئًا، الاختبار يفشل برسالة صريحة.

الخطوة 6 — اكتشاف Xcode Previews

Preview عرض صغير لـ view SwiftUI مُنَفَّذ في canvas Xcode، دون تشغيل simulator. هذه الحلقة شبه فورية: تعديل الكود ورؤية العرض يُحَدَّث في أقلّ من ثانية.

struct CompteurView: View {
    @State private var modele = CompteurVM()

    var body: some View {
        VStack(spacing: 16) {
            Text("Valeur : \(modele.valeur)")
                .font(.title)
            Button("Incrémenter") { modele.incrementer() }
                .buttonStyle(.borderedProminent)
            Button("Réinitialiser") { modele.reinitialiser() }
                .buttonStyle(.bordered)
        }
        .padding()
    }
}

#Preview("Compteur — valeur initiale 0") {
    CompteurView()
}

#Preview("Compteur — valeur initiale 5") {
    let vm = CompteurVM()
    vm.valeur = 5
    return CompteurView()
}

الـ macro #Preview تحلّ محلّ PreviewProvider structs القديمة. عدّة previews في الملفّ نفسه تُنتج عدّة معاينات جنبًا إلى جنب — مفيد لمقارنة حالات (فارغ، ممتلئ، خطأ) أو إعدادات (وضع فاتح/داكن).

الخطوة 7 — وضع تفاعلي واختصارات Preview

canvas Xcode يعرض وضعَين: ساكن (عرض سريع، لا تفاعل) وlive (تفاعل حقيقي، رسوم متحرّكة، حالة). وضع live أبطأ في الإقلاع لكنّه يُتيح النقر في الأزرار، الكتابة في TextFields، التمرير في القوائم.

للتبديل، استعمل الأزرار يمين canvas أو الاختصارات: ⌥⌘P يستأنف preview؛ ⌥⌘⏎ يُبَدِّل عرض canvas؛ النقر على أيقونة Live أسفل اليمين يُفَعِّل التفاعل. في حالة bug، زرّ Resume يفرض re-render نظيف.

الخطوة 8 — ربط Preview ببيانات اختبار

extension CompteurVM {
    static var preview: CompteurVM {
        let vm = CompteurVM()
        vm.valeur = 7
        vm.maximum = 20
        return vm
    }
}

#Preview("Compteur — état pré-rempli") {
    CompteurView()
        .environment(CompteurVM.preview)
}

هذا النمط يُمَركِز بيانات العرض ويتفادى تكرار القيم. لنماذج أكثر تعقيدًا، يمكن وضع هذه stubs في ملفّ PreviewData.swift منفصل، مستثنى من هدف الإنتاج عبر Target Membership.

الخطوة 9 — قياس الأداء بـ Swift Testing

@Test(.timeLimit(.seconds(1)))
func decodageRapide() throws {
    let big = String(repeating: "{\"k\":1},", count: 1000).dropLast()
    let json = "[\(big)]".data(using: .utf8)!
    struct Item: Codable { let k: Int }
    let items = try JSONDecoder().decode([Item].self, from: json)
    #expect(items.count == 1000)
}

إن تجاوز الاختبار الحدّ، يفشل برسالة صريحة. هذه الـ macro مفيدة لاكتشاف انحدار أداء لا يُلاحَظ في التنفيذ على آلة قوية.

الخطوة 10 — تنفيذ الاختبارات من سطر الأوامر

xcodebuild test \
  -project MonApp.xcodeproj \
  -scheme MonApp \
  -destination 'platform=iOS Simulator,name=iPhone 16 Pro'

الأمر يُجَمِّع، يُشَغِّل simulator، يُنَفِّذ هدف الاختبارات، ثم يعرض ملخّصًا نصّيًّا. رمز الخروج غير صفري إن أخفق اختبار، ممّا يُتيح لـ runners CI (GitHub Actions، Bitrise، Xcode Cloud) تعليم الـ pipeline بالإخفاق.

بناء سلسلة اختبارات تبقى مُربحة عبر الوقت

سلسلة اختبارات تصير عبئًا حين تختبر تفاصيل التنفيذ بدل السلوك. العَرَض النمطي: تعديل اسم متغيّر داخلي يكسر عشرين اختبارًا. لتفادي هذا الفخّ، اختبر العقود العامّة — ما تَعِد به دالّة بفعله — لا الآليّة الداخلية. لـ view model، الاختبارات تصف انتقالًا من حالة إلى أخرى نتيجة استدعاء دالّة عامّة.

استراتيجية مكمِّلة تطبيق هرم الاختبارات الكلاسيكي. في القاعدة، اختبارات وحدوية كثيرة سريعة على الدوالّ الصرفة والـ view models. في الوسط، بضع اختبارات تكامل تجمع عدّة وحدات (مثلًا view model يستهلك عميل API محاكى). في القمّة، قليل جدًّا من اختبارات end-to-end (XCUITest يقود simulator). استهداف 80% إلى 90% اختبارات وحدوية و10% إلى 20% تكامل توازن مُجَرَّب.

أخطاء شائعة في اختبارات Swift

الخطأ السبب الحلّ
"No such module 'Testing'" هدف iOS سابق لـ 17 أو Xcode سابق لـ 16 حدِّث الهدف وXcode؛ بديل XCTest للمشاريع القديمة
"Type X is not visible from this module" نسيان @testable import أضف التعليق لفتح وصول رموز internal
Preview يعرض شاشة بيضاء view تنهار صامتًا عند الإنشاء انقر Resume، راقب console Preview، بسِّط الإنشاء
"This expression depends on a variable used outside of its observability scope" التقاط في closure async التقط صراحة بـ [self] أو refactor
اختبار يمرّ محلّيًّا، يفشل في CI فرق نطاق زمني أو locale افرض locale في الاختبار
Preview بطيء في الإقلاع وضع live على view ثقيلة انتقل إلى وضع ساكن للتكرار السريع؛ live فقط لتأكيد التفاعل

أسئلة شائعة

هل Swift Testing يحلّ محلّ XCTest كلّيًّا؟ Apple تضع Swift Testing كالطريق الموصى به للمشاريع الجديدة. XCTest يبقى مدعومًا إلى أجل غير محدّد ويُستعمَل لاختبارات UI (XCUITest لا مكافئ له في Swift Testing حاليًّا). يمكن خلطهما في نفس الهدف.

كيف نعزل الاختبارات؟ كلّ اختبار يُنشئ تبعيّاته في جسده. لتبعيّات مكلفة (قاعدة SQLite، fixture ضخم)، استعمل @Suite(.serialized) مع خاصية مشتركة ودالّة reset بين الاختبارات.

أيمكن mock لـ URLSession في Swift Testing؟ نعم، عبر URLProtocol: أنشئ class URLProtocolMock تعترض الطلبات وتُرجع ردودًا مُحَدَّدة. هذه التقنية تعمل في Swift Testing كما في XCTest.

الفرق بين #expect و#require؟ #expect يُسَجِّل الإخفاق لكن يُكمل تنفيذ الاختبار. #require يوقف الاختبار فورًا إن أخفق الشرط. استعمل #require حين لا معنى لباقي الاختبار إن كان الشرط كاذبًا.

كيف نختبر view SwiftUI ذاتها؟ Xcode Previews أداة المرجع البصرية. لتأكيدات آليّة على العرض (snapshot testing)، مكتبات مثل swift-snapshot-testing تلتقط صورة الـ view وتُقارنها بمرجع.

هل Xcode Previews موثوقة على Mac Intel؟ أقلّ من Apple Silicon. Previews تتطلّب ذاكرة وCPU كثيرة؛ على Intel، يمكن أن تنتهي وقتيًّا أو تنهار. Mac M1 أو لاحق أكثر راحة محسوسًا.

مصادر رسمية

  • Swift Testing — التوثيق الرسمي.
  • SwiftUI وXcode Previews — الصفحة الرسمية.
  • WWDC24 — Meet Swift Testing.
  • repository الرسمي swift-testing.
  • توثيق Previews.

🔝 العودة إلى الدليل الرئيسي.

مقالات ذات صلة

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité