Initial commit: MangaReader iOS App
✨ 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>
This commit is contained in:
415
ios-app/Tests/TestExamples.swift
Normal file
415
ios-app/Tests/TestExamples.swift
Normal file
@@ -0,0 +1,415 @@
|
||||
import XCTest
|
||||
@testable import MangaReader
|
||||
|
||||
/// Ejemplos de cómo escribir tests para el proyecto MangaReader
|
||||
/// Este archivo serve como guía de referencia para crear nuevos tests
|
||||
|
||||
// MARK: - Ejemplo 1: Test Unitario Básico
|
||||
|
||||
final class ExampleTests: XCTestCase {
|
||||
|
||||
// MARK: - Ejemplo: Test simple de modelo
|
||||
|
||||
func testEjemploModeloSimple() {
|
||||
// Arrange: Preparar los datos
|
||||
let manga = Manga(
|
||||
slug: "test",
|
||||
title: "Test Manga",
|
||||
description: "Desc",
|
||||
genres: ["Action"],
|
||||
status: "PUBLICANDOSE",
|
||||
url: "https://example.com"
|
||||
)
|
||||
|
||||
// Act: Ejecutar la acción (si es necesario)
|
||||
let displayTitle = manga.title
|
||||
|
||||
// Assert: Verificar el resultado
|
||||
XCTAssertEqual(displayTitle, "Test Manga")
|
||||
XCTAssertEqual(manga.slug, "test")
|
||||
XCTAssertTrue(manga.genres.contains("Action"))
|
||||
}
|
||||
|
||||
// MARK: - Ejemplo: Test con async/await
|
||||
|
||||
func testEjemploAsync() async throws {
|
||||
// Arrange
|
||||
let storageService = StorageService.shared
|
||||
let image = createTestImage()
|
||||
|
||||
// Act
|
||||
let url = try await storageService.saveImage(
|
||||
image,
|
||||
mangaSlug: "test",
|
||||
chapterNumber: 1,
|
||||
pageIndex: 0
|
||||
)
|
||||
|
||||
// Assert
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
|
||||
}
|
||||
|
||||
// MARK: - Ejemplo: Test de error
|
||||
|
||||
func testEjemploManejoDeError() async {
|
||||
// Arrange
|
||||
let storageService = StorageService.shared
|
||||
|
||||
// Act & Assert
|
||||
do {
|
||||
_ = try await storageService.saveImage(
|
||||
UIImage(), // Imagen vacía
|
||||
mangaSlug: "test",
|
||||
chapterNumber: 1,
|
||||
pageIndex: 0
|
||||
)
|
||||
XCTFail("Debería haber lanzado un error")
|
||||
} catch {
|
||||
XCTAssertNotNil(error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Ejemplo: Test con helpers
|
||||
|
||||
func testEjemploConHelpers() {
|
||||
// Usar TestDataFactory para crear datos de prueba
|
||||
let manga = TestDataFactory.createManga(
|
||||
slug: "mi-manga",
|
||||
title: "Mi Manga",
|
||||
genres: ["Action", "Fantasy"]
|
||||
)
|
||||
|
||||
// Usar AssertionHelpers
|
||||
AssertionHelpers.assertValidManga(manga)
|
||||
XCTAssertTrue(manga.genres.count == 2)
|
||||
}
|
||||
|
||||
// MARK: - Ejemplo: Test de múltiples escenarios
|
||||
|
||||
func testEjemploMultiplesEscenarios() {
|
||||
// Escenario 1: Manga publicado
|
||||
let publishedManga = TestDataFactory.createManga(
|
||||
status: "PUBLICANDOSE"
|
||||
)
|
||||
XCTAssertEqual(publishedManga.displayStatus, "En publicación")
|
||||
|
||||
// Escenario 2: Manga finalizado
|
||||
let finishedManga = TestDataFactory.createManga(
|
||||
status: "FINALIZADO"
|
||||
)
|
||||
XCTAssertEqual(finishedManga.displayStatus, "Finalizado")
|
||||
|
||||
// Escenario 3: Manga en pausa
|
||||
let pausedManga = TestDataFactory.createManga(
|
||||
status: "EN_PAUSA"
|
||||
)
|
||||
XCTAssertEqual(pausedManga.displayStatus, "En pausa")
|
||||
}
|
||||
|
||||
// MARK: - Ejemplo: Test de performance
|
||||
|
||||
func testEjemploPerformance() {
|
||||
let manga = TestDataFactory.createManga()
|
||||
|
||||
// Medir cuánto tarda en codificar 1000 veces
|
||||
measure {
|
||||
for _ in 0..<1000 {
|
||||
_ = try? JSONEncoder().encode(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Ejemplo: Test con setup/teardown
|
||||
|
||||
var storageService: StorageService!
|
||||
|
||||
override func setUp() async throws {
|
||||
try await super.setUp()
|
||||
// Se ejecuta antes de cada test
|
||||
storageService = StorageService.shared
|
||||
StorageTestHelpers.clearAllStorage()
|
||||
}
|
||||
|
||||
override func tearDown() async throws {
|
||||
// Se ejecuta después de cada test
|
||||
StorageTestHelpers.clearAllStorage()
|
||||
storageService = nil
|
||||
try await super.tearDown()
|
||||
}
|
||||
|
||||
func testEjemploConSetup() {
|
||||
// El storageService ya está inicializado y limpio
|
||||
storageService.saveFavorite(mangaSlug: "test")
|
||||
XCTAssertTrue(storageService.isFavorite(mangaSlug: "test"))
|
||||
}
|
||||
|
||||
// MARK: - Ejemplo: Test de integración
|
||||
|
||||
func testEjemploIntegracion() async throws {
|
||||
// Simular flujo completo del usuario
|
||||
|
||||
// 1. Usuario busca un manga
|
||||
let manga = TestDataFactory.createManga(slug: "tower-of-god")
|
||||
|
||||
// 2. Usuario lo agrega a favoritos
|
||||
storageService.saveFavorite(mangaSlug: manga.slug)
|
||||
XCTAssertTrue(storageService.isFavorite(mangaSlug: manga.slug))
|
||||
|
||||
// 3. Usuario comienza a leer
|
||||
let progress = ReadingProgress(
|
||||
mangaSlug: manga.slug,
|
||||
chapterNumber: 1,
|
||||
pageNumber: 0,
|
||||
timestamp: Date()
|
||||
)
|
||||
storageService.saveReadingProgress(progress)
|
||||
|
||||
// 4. Verificar que todo se guardó correctamente
|
||||
let retrieved = storageService.getReadingProgress(
|
||||
mangaSlug: manga.slug,
|
||||
chapterNumber: 1
|
||||
)
|
||||
XCTAssertNotNil(retrieved)
|
||||
XCTAssertEqual(retrieved?.pageNumber, 0)
|
||||
}
|
||||
|
||||
// MARK: - Ejemplo: Test concurrente
|
||||
|
||||
func testEjemploConcurrencia() async throws {
|
||||
// Crear múltiples tareas concurrentes
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
for i in 0..<10 {
|
||||
group.addTask {
|
||||
let manga = TestDataFactory.createManga(slug: "manga-\(i)")
|
||||
self.storageService.saveFavorite(mangaSlug: manga.slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar que todas se guardaron
|
||||
let favorites = storageService.getFavorites()
|
||||
XCTAssertEqual(favorites.count, 10)
|
||||
}
|
||||
|
||||
// MARK: - Ejemplo: Test con mock
|
||||
|
||||
func testEjemploConMock() {
|
||||
// Mock de respuesta de JavaScript
|
||||
let mockResponse: [[String: Any]] = [
|
||||
["number": 1, "title": "Chapter 1", "url": "url1", "slug": "slug1"],
|
||||
["number": 2, "title": "Chapter 2", "url": "url2", "slug": "slug2"]
|
||||
]
|
||||
|
||||
// Parsear respuesta mock
|
||||
let chapters = mockResponse.compactMap { dict -> Chapter? in
|
||||
guard let number = dict["number"] as? Int,
|
||||
let title = dict["title"] as? String,
|
||||
let url = dict["url"] as? String,
|
||||
let slug = dict["slug"] as? String else {
|
||||
return nil
|
||||
}
|
||||
return Chapter(number: number, title: title, url: url, slug: slug)
|
||||
}
|
||||
|
||||
// Verificar parsing
|
||||
XCTAssertEqual(chapters.count, 2)
|
||||
XCTAssertEqual(chapters[0].number, 1)
|
||||
XCTAssertEqual(chapters[1].number, 2)
|
||||
}
|
||||
|
||||
// MARK: - Ejemplo: Test de edge cases
|
||||
|
||||
func testEjemploEdgeCases() {
|
||||
// Caso 1: String vacío
|
||||
let emptyManga = TestDataFactory.createManga(title: "")
|
||||
XCTAssertEqual(emptyManga.title, "")
|
||||
|
||||
// Caso 2: Array vacío
|
||||
let noGenresManga = TestDataFactory.createManga(genres: [])
|
||||
XCTAssertTrue(noGenresManga.genres.isEmpty)
|
||||
|
||||
// Caso 3: Valor negativo
|
||||
let chapter = TestDataFactory.createChapter(number: -1)
|
||||
XCTAssertEqual(chapter.number, -1)
|
||||
|
||||
// Caso 4: Caracteres especiales
|
||||
let specialSlug = "manga-áéíóú-ñ-@#$"
|
||||
storageService.saveFavorite(mangaSlug: specialSlug)
|
||||
XCTAssertTrue(storageService.isFavorite(mangaSlug: specialSlug))
|
||||
}
|
||||
|
||||
// MARK: - Ejemplo: Test con assertions personalizadas
|
||||
|
||||
func testEjemploAssertionsPersonalizadas() {
|
||||
let manga1 = TestDataFactory.createManga(slug: "test")
|
||||
let manga2 = TestDataFactory.createManga(slug: "test")
|
||||
|
||||
// Usar assert personalizado
|
||||
XCTAssertEqual(manga1, manga2)
|
||||
XCTAssertEqual(manga1.hashValue, manga2.hashValue)
|
||||
|
||||
// Verificar URL válida
|
||||
AssertionHelpers.assertValidURL(manga1.url)
|
||||
|
||||
// Verificar manga válido
|
||||
AssertionHelpers.assertValidManga(manga1)
|
||||
}
|
||||
|
||||
// MARK: - Helpers para ejemplos
|
||||
|
||||
private func createTestImage() -> UIImage {
|
||||
let size = CGSize(width: 800, height: 1200)
|
||||
UIGraphicsBeginImageContext(size)
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
context?.setFillColor(UIColor.blue.cgColor)
|
||||
context?.fill(CGRect(origin: .zero, size: size))
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return image!
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Plantillas de Tests
|
||||
|
||||
/// Plantilla para test unitario simple
|
||||
/*
|
||||
func test[NombreFuncionalidad]_[Condición]_[ResultadoEsperado]() {
|
||||
// Arrange
|
||||
let [input] = [valor]
|
||||
|
||||
// Act
|
||||
let result = [función](input)
|
||||
|
||||
// Assert
|
||||
XCTAssertEqual(result, [esperado])
|
||||
}
|
||||
*/
|
||||
|
||||
/// Plantilla para test asíncrono
|
||||
/*
|
||||
func test[NombreFuncionalidad]_Async() async throws {
|
||||
// Arrange
|
||||
let [input] = [valor]
|
||||
|
||||
// Act
|
||||
let result = try await [funciónAsync](input)
|
||||
|
||||
// Assert
|
||||
XCTAssertNotNil(result)
|
||||
}
|
||||
*/
|
||||
|
||||
/// Plantilla para test de error
|
||||
/*
|
||||
func test[NombreFuncionalidad]_ThrowsError() {
|
||||
// Arrange
|
||||
let [inputInvalido] = [valor]
|
||||
|
||||
// Act & Assert
|
||||
XCTAssertThrowsError(
|
||||
try [función](inputInvalido)
|
||||
) { error in
|
||||
XCTAssertEqual(error as! [TipoError], [errorEsperado])
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/// Plantilla para test de performance
|
||||
/*
|
||||
func test[NombreFuncionalidad]_Performance() {
|
||||
let [data] = [crearDatosDePrueba]
|
||||
|
||||
measure {
|
||||
_ = [función](data)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/// Plantilla para test de integración
|
||||
/*
|
||||
func test[FlujoCompleto]_Integration() async throws {
|
||||
// Paso 1: [acción inicial]
|
||||
let [result1] = try await [función1]()
|
||||
|
||||
// Paso 2: [acción siguiente]
|
||||
let [result2] = [función2](result1)
|
||||
|
||||
// Paso 3: [verificación final]
|
||||
XCTAssertNotNil([resultFinal])
|
||||
XCTAssertTrue([condición])
|
||||
}
|
||||
*/
|
||||
|
||||
// MARK: - Consejos para Escribir Tests
|
||||
|
||||
/*
|
||||
✅ BUENOS HÁBITOS:
|
||||
|
||||
1. Usa nombres descriptivos:
|
||||
- testSaveFavorite_AddsNewFavorite_WhenNotExists
|
||||
- testChapterProgress_ReturnsDouble_WhenSet
|
||||
|
||||
2. Un assert por test:
|
||||
- Separa en múltiples tests si hay varios asserts
|
||||
- Usa subtests si están relacionados
|
||||
|
||||
3. Arrange-Act-Assert:
|
||||
- Arrange: Prepara los datos
|
||||
- Act: Ejecuta la acción
|
||||
- Assert: Verifica el resultado
|
||||
|
||||
4. Tests independientes:
|
||||
- No dependen del orden de ejecución
|
||||
- Limpian después de sí mismos
|
||||
|
||||
5. Usa helpers:
|
||||
- TestDataFactory para crear objetos
|
||||
- AssertionHelpers para verificar condiciones
|
||||
|
||||
❌ MALOS HÁBITOS:
|
||||
|
||||
1. Tests con múltiples asserts no relacionados
|
||||
2. Tests que dependen del orden
|
||||
3. Tests que no limpian después
|
||||
4. Tests con nombres ambiguos
|
||||
5. Tests que llaman a APIs reales
|
||||
*/
|
||||
|
||||
// MARK: - Referencias Rápidas
|
||||
|
||||
/*
|
||||
COMUNES ASSERTIONS:
|
||||
|
||||
- XCTAssertEqual(a, b) - Verifica igualdad
|
||||
- XCTAssertNotEqual(a, b) - Verifica desigualdad
|
||||
- XCTAssertTrue(condición) - Verifica true
|
||||
- XCTAssertFalse(condición) - Verifica false
|
||||
- XCTAssertNil(valor) - Verifica nil
|
||||
- XCTAssertNotNil(valor) - Verifica no nil
|
||||
- XCTAssertThrowsError(expr) - Verifica que lanza error
|
||||
- XCTAssertNoThrow(expr) - Verifica que NO lanza error
|
||||
|
||||
ASYNC/AWAIT:
|
||||
|
||||
- try await [función async] - Ejecutar función async
|
||||
- try await Task.sleep(...) - Esperar
|
||||
- await withTaskGroup {} - Tareas concurrentes
|
||||
|
||||
SETUP/TEARDOWN:
|
||||
|
||||
- override func setUp() - Antes de cada test
|
||||
- override func tearDown() - Después de cada test
|
||||
- override func setUpWithError() - Con errores
|
||||
- override func tearDownWithError() - Con errores
|
||||
|
||||
PERFORMANCE:
|
||||
|
||||
- measure { } - Medir bloques de código
|
||||
- measure(metrics: ...) { } - Métricas específicas
|
||||
|
||||
MOCKS:
|
||||
|
||||
- TestDataFactory - Crear objetos de prueba
|
||||
- ImageTestHelpers - Crear imágenes
|
||||
- FileSystemTestHelpers - Operaciones de archivos
|
||||
*/
|
||||
Reference in New Issue
Block a user