✨ 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>
416 lines
12 KiB
Swift
416 lines
12 KiB
Swift
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
|
|
*/
|