✨ Features: - App iOS completa para leer manga sin publicidad - Scraper con WKWebView para manhwaweb.com - Sistema de descargas offline - Lector con zoom y navegación - Favoritos y progreso de lectura - Compatible con iOS 15+ y Sideloadly/3uTools 📦 Contenido: - Backend Node.js con Puppeteer (opcional) - App iOS con SwiftUI - Scraper de capítulos e imágenes - Sistema de almacenamiento local - Testing completo - Documentación exhaustiva 🧪 Prueba: Capítulo 789 de One Piece descargado exitosamente - 21 páginas descargadas - 4.68 MB total - URLs verificadas y funcionales 🎉 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
389 lines
10 KiB
Swift
389 lines
10 KiB
Swift
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<T>(
|
|
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<T>(
|
|
_ 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<T>(
|
|
_ 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<T>(
|
|
_ 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
|