# Regla: Testing en Swift / iOS

Esta regla es OBLIGATORIA para todo código Swift nuevo o modificado.
La orientación a protocolos de Swift permite mocks sin frameworks de generación.
Swift Testing (Swift 6) es el estándar para código nuevo.

---

## Stack de testing obligatorio

| Herramienta | Propósito |
|------------|----------|
| XCTest | Framework base (compatible con todas las versiones) |
| swift-testing | Framework moderno para código Swift 6+ |
| swift-snapshot-testing | Tests de regresión visual para vistas |
| XCUITest | Tests de UI end-to-end |
| Xcode Coverage | Reporte de cobertura integrado en Xcode |

```swift
// Package.swift — dependencias de test
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.16.0"),
.package(url: "https://github.com/apple/swift-testing", from: "0.10.0"),
```

---

## swift-testing para código Swift 6 nuevo

`swift-testing` usa macros (`@Test`, `@Suite`) y es más expresivo que XCTest.
Para proyectos que soportan iOS 16 o anterior, usar XCTest.

```swift
import Testing

// BIEN — swift-testing con macros
@Suite("PedidoViewModel")
struct PedidoViewModelTests {

    @Test("Carga pedidos correctamente")
    func cargarPedidosExitoso() async throws {
        // Given
        let repositorioMock = MockPedidoRepository(pedidos: [.ejemplo])
        let viewModel = PedidoViewModel(repositorio: repositorioMock)

        // When
        await viewModel.cargar()

        // Then
        #expect(viewModel.pedidos.count == 1)
        #expect(!viewModel.isLoading)
    }

    @Test("Maneja error de red")
    func cargarPedidosConError() async throws {
        // Given
        let repositorioMock = MockPedidoRepository(error: URLError(.notConnectedToInternet))
        let viewModel = PedidoViewModel(repositorio: repositorioMock)

        // When
        await viewModel.cargar()

        // Then
        #expect(viewModel.error != nil)
        #expect(viewModel.pedidos.isEmpty)
    }
}
```

---

## Mocks basados en protocolos (sin frameworks de generación)

La orientación a protocolos de Swift hace innecesarios los frameworks de mocking.
Implementar mocks manualmente: son simples, legibles y sin dependencias.

```swift
// Protocolo de produccion
protocol PedidoRepository {
    func obtenerTodos() async throws -> [Pedido]
    func guardar(_ pedido: Pedido) async throws
}

// Mock manual — simple y declarativo
final class MockPedidoRepository: PedidoRepository {
    var pedidos: [Pedido] = []
    var errorALanzar: Error?
    private(set) var guardarLlamado = false
    private(set) var pedidoGuardado: Pedido?

    func obtenerTodos() async throws -> [Pedido] {
        if let error = errorALanzar { throw error }
        return pedidos
    }

    func guardar(_ pedido: Pedido) async throws {
        guardarLlamado = true
        pedidoGuardado = pedido
        if let error = errorALanzar { throw error }
    }
}

// Uso en test
let mock = MockPedidoRepository()
mock.pedidos = [Pedido(id: UUID(), total: 150)]
let vm = PedidoViewModel(repositorio: mock)
await vm.cargar()
XCTAssertEqual(vm.pedidos.count, 1)
```

---

## Patrón Given-When-Then en XCTest

```swift
import XCTest

final class CrearPedidoUseCaseTests: XCTestCase {

    private var repositorioMock: MockPedidoRepository!
    private var useCase: CrearPedidoUseCase!

    override func setUp() {
        super.setUp()
        repositorioMock = MockPedidoRepository()
        useCase = CrearPedidoUseCase(repositorio: repositorioMock)
    }

    override func tearDown() {
        repositorioMock = nil
        useCase = nil
        super.tearDown()
    }

    func test_crearPedido_conItemsValidos_guardaYRetornaPedido() async throws {
        // Given
        let items = [ItemPedido(productoId: UUID(), cantidad: 2)]

        // When
        let pedido = try await useCase.ejecutar(items: items)

        // Then
        XCTAssertEqual(pedido.items.count, 1)
        XCTAssertTrue(repositorioMock.guardarLlamado)
        XCTAssertEqual(repositorioMock.pedidoGuardado?.id, pedido.id)
    }

    func test_crearPedido_sinItems_lanzaError() async {
        // Given
        let itemsVacios: [ItemPedido] = []

        // When / Then
        await XCTAssertThrowsError(
            try await useCase.ejecutar(items: itemsVacios)
        ) { error in
            XCTAssertTrue(error is PedidoError)
        }
    }
}
```

---

## Snapshot testing para regresión visual

```swift
import SnapshotTesting

final class PantallaPedidosSnapshotTests: XCTestCase {

    func test_pantallaPedidos_estadoSuccess() {
        // Given
        let vm = PedidoViewModel(repositorio: MockPedidoRepository(pedidos: [.ejemplo]))
        let vista = PantallaPedidos(viewModel: vm)
        let vc = UIHostingController(rootView: vista)

        // Then — genera o compara snapshot
        assertSnapshot(of: vc, as: .image(on: .iPhone13Pro))
    }
}
```

Los archivos de snapshot se versionan en git bajo `__Snapshots__/`.
Un snapshot que falla indica un cambio visual inesperado — revisar antes de actualizar.

---

## Tests de UI con XCUITest

```swift
final class FlujoCrearPedidoUITests: XCTestCase {

    var app: XCUIApplication!

    override func setUp() {
        super.setUp()
        continueAfterFailure = false
        app = XCUIApplication()
        app.launchArguments = ["--uitesting"]  // modo de test con datos mock
        app.launch()
    }

    func test_crearPedido_flujoCompleto() {
        // Given — pantalla de lista de pedidos
        let botonNuevo = app.buttons["nuevo_pedido"]
        XCTAssertTrue(botonNuevo.waitForExistence(timeout: 3))

        // When — crear pedido
        botonNuevo.tap()
        app.textFields["campo_descripcion"].typeText("Pedido de prueba")
        app.buttons["confirmar_pedido"].tap()

        // Then — verificar que aparece en la lista
        let textoPedido = app.staticTexts["Pedido de prueba"]
        XCTAssertTrue(textoPedido.waitForExistence(timeout: 5))
    }
}
```

---

## Cobertura con Xcode Coverage

Habilitar en el esquema de Xcode: `Edit Scheme → Test → Code Coverage → Gather coverage`.

Objetivo de cobertura por capa:
- Capa de dominio (use cases, modelos): >= 90%
- Capa de datos (repositorios, mappers): >= 80%
- Capa de presentación (ViewModels): >= 80%
- Vistas SwiftUI: snapshot tests cubren regresión visual

---

## Checklist de testing Swift antes de abrir PR

- [ ] Tests usan `swift-testing` (Swift 6) o `XCTest` según target de deployment
- [ ] Mocks implementados manualmente vía protocolos (sin frameworks)
- [ ] Patrón Given-When-Then con comentarios explícitos
- [ ] Cobertura de dominio y ViewModels >= 80%
- [ ] Snapshots generados y versionados para vistas críticas
- [ ] Tests de UI para flujos críticos con XCUITest
- [ ] Sin `Thread.sleep()` ni delays fijos: usar `Task` y `await`
- [ ] `setUp`/`tearDown` limpian estado entre tests
