MVVM vs The Composable Architecture (TCA)
نمطان معماريان من الأكثر شيوعًا في iOS وجهًا لوجه: MVVM التقليدي مقابل TCA الوظيفي (functional) من Point-Free. كيف تختار المعمارية الصحيحة؟
السرعة، العزل، وحلقة تغذية راجعة موثوقة
تفاعل حقيقي بين المكونات، وتحقق شامل من طرف إلى طرف
| الفئة | Unit Test | Integration Test |
|---|---|---|
| الأداء | 10/10 | 4/10 |
| سهولة التعلّم | 8/10 | 6/10 |
| النظام البيئي | 9/10 | 8/10 |
| المجتمع | 9/10 | 8/10 |
| سوق العمل | 9/10 | 8/10 |
| الاستدامة المستقبلية | 9/10 | 9/10 |
// Swift - أمثلة اختبار وحدوي (XCTest + Swift Testing)
import Testing
import Foundation
@testable import MyApp
// إطار Swift Testing (iOS 17+, WWDC 2024)
struct PriceFormatterTests {
@Test("تنسيق الليرة التركية يجب أن يكون صحيحاً")
func turkishLiraFormat() {
let formatter = PriceFormatter(locale: Locale(identifier: "tr_TR"))
#expect(formatter.format(1234.5) == "₺1.234,50")
#expect(formatter.format(0) == "₺0,00")
#expect(formatter.format(-50) == "-₺50,00")
}
@Test("السعر غير الصالح يجب ألا يكون سالباً",
arguments: [-1.0, -100.0, -0.01])
func negativePriceValidation(price: Double) {
let validator = PriceValidator()
#expect(!validator.isValid(price))
}
}
struct CartViewModelTests {
var sut: CartViewModel!
var mockRepository: MockCartRepository!
@Test("السعر الإجمالي يجب أن يتحدث عند إضافة منتج")
mutating func addProductUpdatesTotalPrice() async throws {
mockRepository = MockCartRepository()
sut = CartViewModel(repository: mockRepository)
let product = Product(id: "p1", name: "MacBook", price: 75000)
await sut.addProduct(product)
#expect(sut.totalPrice == 75000)
#expect(sut.itemCount == 1)
}
@Test("الكمية يجب أن تزداد عند إضافة نفس المنتج مرتين")
mutating func addSameProductIncreasesQuantity() async throws {
mockRepository = MockCartRepository()
sut = CartViewModel(repository: mockRepository)
let product = Product(id: "p1", name: "MacBook", price: 75000)
await sut.addProduct(product)
await sut.addProduct(product)
#expect(sut.items.count == 1)
#expect(sut.items.first?.quantity == 2)
#expect(sut.totalPrice == 150000)
}
}
// تنفيذ وهمي (Mock)
class MockCartRepository: CartRepositoryProtocol {
var savedItems: [CartItem] = []
func save(_ item: CartItem) async throws {
savedItems.append(item)
}
func fetchAll() async throws -> [CartItem] {
return savedItems
}
}// Swift - أمثلة اختبار تكامل
import XCTest
@testable import MyApp
// اختبار تكامل باستخدام SQLite في الذاكرة
class UserRepositoryIntegrationTests: XCTestCase {
var repository: UserRepository!
var database: TestDatabase!
override func setUp() async throws {
// نستخدم قاعدة بيانات SQLite حقيقية في الذاكرة
database = try await TestDatabase.inMemory()
repository = UserRepository(database: database)
}
override func tearDown() async throws {
try await database.cleanup()
database = nil
repository = nil
}
func testCreateAndFetchUser() async throws {
// إنشاء
let userId = try await repository.createUser(
name: "Ahmet Yılmaz",
email: "[email protected]"
)
// جلب
let fetchedUser = try await repository.fetchUser(id: userId)
XCTAssertNotNil(fetchedUser)
XCTAssertEqual(fetchedUser?.name, "Ahmet Yılmaz")
XCTAssertEqual(fetchedUser?.email, "[email protected]")
}
func testDeleteUserCascadesToPosts() async throws {
let userId = try await repository.createUser(name: "Test", email: "[email protected]")
let postRepo = PostRepository(database: database)
_ = try await postRepo.createPost(title: "Post 1", userId: userId)
_ = try await postRepo.createPost(title: "Post 2", userId: userId)
// حذف المستخدم
try await repository.deleteUser(id: userId)
// يجب حذف منشورات المستخدم أيضاً (cascade)
let posts = try await postRepo.fetchPosts(userId: userId)
XCTAssertTrue(posts.isEmpty, "حذف المستخدم يجب أن يحذف منشوراته أيضاً")
}
}
// اختبار تكامل شبكة باستخدام URLSession حقيقي
class APIClientIntegrationTests: XCTestCase {
func testFetchPublicAPIData() async throws {
// يستخدم هذا الاختبار اتصال شبكة حقيقي
// يجب أن يعمل فقط في بيئات CI المتصلة بالشبكة
try XCTSkipUnless(ProcessInfo.processInfo.environment["INTEGRATION_TESTS"] == "1")
let client = APIClient(baseURL: URL(string: "https://jsonplaceholder.typicode.com")!)
let posts: [Post] = try await client.request(path: "/posts")
XCTAssertFalse(posts.isEmpty)
XCTAssertEqual(posts.count, 100)
}
}هرم الاختبار المثالي: عدد كبير من الاختبارات الوحدوية (70%)، وعدد أقل من اختبارات التكامل (20%)، والحد الأدنى من اختبارات الواجهة/الشاملة (10%). استخدم الاختبارات الوحدوية لتحصل على تغذية راجعة سريعة، واختبارات التكامل للتحقق من المسارات الحرجة معاً — لا تستبدل أحدهما بالآخر بل استخدمهما كعنصرين متكاملين، فتلك هي الاستراتيجية الصحيحة.
احصل على استشارة مجانيةلا يوجد رقم سحري. تغطية تتجاوز 80% لمنطق العمل (business logic) هدف جيد. لكن الأهم ليس نسبة التغطية بل اختبار الأشياء الصحيحة — والسعي الأعمى لتغطية 100% قد يكون ضاراً.