MVVM vs The Composable Architecture (TCA)
Los dos patrones de arquitectura más populares en iOS se enfrentan: el clásico MVVM frente al TCA funcional de Point-Free. ¿Cómo eliges la arquitectura correcta?
Velocidad, aislamiento y ciclo de feedback confiable
Interacción real de componentes, validación de extremo a extremo
| Categoría | Unit Test | Integration Test |
|---|---|---|
| Rendimiento | 10/10 | 4/10 |
| Facilidad de aprendizaje | 8/10 | 6/10 |
| Ecosistema | 9/10 | 8/10 |
| Comunidad | 9/10 | 8/10 |
| Mercado laboral | 9/10 | 8/10 |
| A prueba de futuro | 9/10 | 9/10 |
// Swift - Ejemplos de tests unitarios (XCTest + Swift Testing)
import Testing
import Foundation
@testable import MyApp
// Framework Swift Testing (iOS 17+, WWDC 2024)
struct PriceFormatterTests {
@Test("El formato de la lira turca debe ser correcto")
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("Un precio inválido no debe ser negativo",
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("El precio total debe actualizarse al añadir un producto")
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("La cantidad debe aumentar al añadir el mismo producto dos veces")
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)
}
}
// Implementación mock
class MockCartRepository: CartRepositoryProtocol {
var savedItems: [CartItem] = []
func save(_ item: CartItem) async throws {
savedItems.append(item)
}
func fetchAll() async throws -> [CartItem] {
return savedItems
}
}// Swift - Ejemplos de tests de integración
import XCTest
@testable import MyApp
// Test de integración con SQLite en memoria
class UserRepositoryIntegrationTests: XCTestCase {
var repository: UserRepository!
var database: TestDatabase!
override func setUp() async throws {
// Usamos una base de datos SQLite real en memoria
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 {
// Crear
let userId = try await repository.createUser(
name: "Ahmet Yılmaz",
email: "[email protected]"
)
// Obtener
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)
// Eliminar el usuario
try await repository.deleteUser(id: userId)
// Sus posts también deben eliminarse (cascade)
let posts = try await postRepo.fetchPosts(userId: userId)
XCTAssertTrue(posts.isEmpty, "Al eliminar el usuario, sus posts también deben eliminarse")
}
}
// Test de integración de red con URLSession real
class APIClientIntegrationTests: XCTestCase {
func testFetchPublicAPIData() async throws {
// Este test usa una conexión de red real
// En CI solo debe ejecutarse en entornos con conectividad
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)
}
}Pirámide de testing: muchos tests unitarios (70%), pocos tests de integración (20%), un mínimo de tests UI/E2E (10%). Usa los tests unitarios para feedback rápido y los de integración para validar flujos críticos, de forma complementaria. La estrategia correcta no es sustituir uno por el otro, sino usarlos juntos.
Solicita una consultoría gratuitaNo hay un número mágico. Un 80%+ de cobertura de la lógica de negocio es un buen objetivo. Pero lo importante no es el porcentaje de cobertura sino probar las cosas correctas. Perseguir ciegamente el 100% de cobertura puede ser contraproducente.