import XCTest /// Extensiones y configuraciones adicionales para XCTest /// Proporciona funcionalidades adicionales para los tests extension XCTestCase { /// Espera un periodo de tiempo específico (útil para operaciones asíncronas) func wait(for duration: TimeInterval) async { try? await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) } /// Ejecuta una operación y espera que complete func waitForOperation( timeout: TimeInterval = 5.0, operation: @escaping () -> T?, file: StaticString = #file, line: UInt = #line ) -> T? { let expectation = self.expectation(description: "Operation completed") var result: T? DispatchQueue.global().async { result = operation() expectation.fulfill() } waitForExpectations(timeout: timeout) { error in if let error = error { XCTFail("Operation timed out: \(error.localizedDescription)", file: file, line: line) } } return result } /// Verifica que una operación lanza un error específico func assertThrowsError( _ error: Error, in expression: () throws -> T, file: StaticString = #file, line: UInt = #line ) { XCTAssertThrowsError( try expression(), file: file, line: line ) { thrownError in XCTAssertEqual( thrownError as? Error, error, "Expected error does not match thrown error", file: file, line: line ) } } /// Ejecuta un test multiple veces para detectar fallos intermitentes func repeatTest( _ count: Int = 10, file: StaticString = #file, line: UInt = #line, test: () throws -> Void ) { var failures = 0 for iteration in 1...count { do { try test() } catch { failures += 1 print("Test failed on iteration \(iteration): \(error)") } } XCTAssertEqual( failures, 0, "Test failed \(failures) out of \(count) times", file: file, line: line ) } } // MARK: - Custom Assertions extension XCTestCase { /// Afirma que un closure no lanza error func assertNoThrow( _ expression: () throws -> Void, file: StaticString = #file, line: UInt = #line ) { do { try expression() } catch { XCTFail( "Unexpected error thrown: \(error.localizedDescription)", file: file, line: line ) } } /// Afirma que dos fechas son aproximadamente iguales (dentro de un margen) func assertDatesEqual( _ date1: Date, _ date2: Date, precision: TimeInterval = 0.001, file: StaticString = #file, line: UInt = #line ) { let difference = abs(date1.timeIntervalSince(date2)) XCTAssertLessThanOrEqual( difference, precision, "Dates are not equal within \(precision)s", file: file, line: line ) } /// Afirma que un array contiene un número específico de elementos func assertCount( _ count: Int, _ array: [Any], file: StaticString = #file, line: UInt = #line ) { XCTAssertEqual( array.count, count, "Array count mismatch", file: file, line: line ) } /// Afirma que una colección está vacía func assertEmpty( _ collection: T, file: StaticString = #file, line: UInt = #line ) where T: Collection { XCTAssertTrue( collection.isEmpty, "Collection should be empty but has \(collection.count) elements", file: file, line: line ) } /// Afirma que una colección no está vacía func assertNotEmpty( _ collection: T, file: StaticString = #file, line: UInt = #line ) where T: Collection { XCTAssertFalse( collection.isEmpty, "Collection should not be empty", file: file, line: line ) } } // MARK: - Memory Leak Detection extension XCTestCase { /// Detecta memory leaks en un objeto func assertNoMemoryLeak( _ instance: AnyObject, file: StaticString = #file, line: UInt = #line ) { addTeardownBlock { [weak instance] in XCTAssertNil( instance, "Instance should be deallocated but still exists (potential memory leak)", file: file, line: line ) } } } // MARK: - Test Logging extension XCTestCase { /// Registra información de depuración durante los tests func logTest(_ message: String, level: LogLevel = .info) { let prefix = "[Test \(level.description)]" print("\(prefix) \(message)") #if DEBUG let testRun = XCTRunLoop.current.currentTestRun print("Test: \(testRun?.test.name ?? "Unknown") - \(message)") #endif } enum LogLevel { case info case warning case error var description: String { switch self { case .info: return "INFO" case .warning: return "WARNING" case .error: return "ERROR" } } } } // MARK: - Test Data Cleanup extension XCTestCase { /// Limpia todos los UserDefaults func clearAllUserDefaults() { let dictionary = UserDefaults.standard.dictionaryRepresentation() dictionary.keys.forEach { key in UserDefaults.standard.removeObject(forKey: key) } } /// Limpia todos los archivos en el directorio temporal func clearTemporaryDirectory() { let tempDir = FileManager.default.temporaryDirectory guard let contents = try? FileManager.default.contentsOfDirectory( at: tempDir, includingPropertiesForKeys: nil ) else { return } for file in contents { try? FileManager.default.removeItem(at: file) } } } // MARK: - Custom Test Runners /// Configuración para ejecutar todos los tests class AllTests { static func runAllTests() { print("🧪 Running MangaReader Test Suite") print("=" * 50) // Tests de Modelos print("📦 Running Model Tests...") // ModelTests se ejecutan automáticamente // Tests de Storage print("💾 Running Storage Service Tests...") // StorageServiceTests se ejecutan automáticamente // Tests de Scraper print("🌐 Running Scraper Tests...") // ManhwaWebScraperTests se ejecutan automáticamente // Tests de Integración print("🔗 Running Integration Tests...") // IntegrationTests se ejecutan automáticamente print("=" * 50) print("✅ All tests completed!") } } // MARK: - Test Metrics extension XCTestCase { /// Registra métricas de rendimiento func recordMetric(_ name: String, value: Double, unit: String = "s") { #if DEBUG let metric = [ "name": name, "value": value, "unit": unit, "timestamp": Date().timeIntervalSince1970 ] as [String : Any] print("📊 Metric: \(name) = \(value) \(unit)") #endif } /// Compara métricas entre runs func assertMetricImproved( _ name: String, currentValue: Double, previousValue: Double, file: StaticString = #file, line: UInt = #line ) { let improvement = ((previousValue - currentValue) / previousValue) * 100 XCTAssertGreaterThan( improvement, 0, "Metric '\(name)' did not improve. Previous: \(previousValue), Current: \(currentValue)", file: file, line: line ) print("✨ Metric '\(name)' improved by \(String(format: "%.2f", improvement))%") } } // MARK: - String Repetition Helper extension String { static func * (left: String, right: Int) -> String { guard right > 0 else { return "" } return String(repeating: left, count: right) } } // MARK: - Test Documentation /* Guía de Ejecución de Tests: 1. Ejecutar todos los tests: - Cmd + U en Xcode - O seleccionar Product > Test 2. Ejecutar tests específicos: - Click derecho en el test específico > Run - Usar el Test Navigator (Cmd + 6) 3. Ejecutar tests con cobertura: - Edit Scheme > Test > Options > Gather coverage - Cmd + U 4. Tests Performance: - Los tests de performance se marcan con "measure" - Se ejecutan 10 veces por defecto - Resultados en el Report Navigator Estructura de Tests: - ModelTests: Pruebas para modelos de datos (Manga, Chapter, etc.) - StorageServiceTests: Pruebas para almacenamiento local - ManhwaWebScraperTests: Pruebas para el web scraper - IntegrationTests: Pruebas de integración completa Mejores Prácticas: 1. Cada test debe ser independiente 2. Los tests deben poder ejecutarse en cualquier orden 3. Usar setUp/tearDown para limpieza 4. Usar nombres descriptivos para los tests 5. Un assert por test (cuando sea posible) 6. Mock de dependencias externas 7. Evitar llamadas de red reales en tests unitarios Marcas de Tests: - @MainActor: Tests que requieren MainActor - async: Tests asíncronos - throws: Tests que pueden lanzar errores */ // MARK: - Custom Test Constraints #if DEBUG /// Contenedor para configuración de tests struct TestConfiguration { static var isRunningTests: Bool { return NSClassFromString("XCTest") != nil } static var testTimeout: TimeInterval = 10.0 static var useMockData: Bool = true static var verboseLogging: Bool = true } #endif