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:
426
ios-app/Tests/README.md
Normal file
426
ios-app/Tests/README.md
Normal file
@@ -0,0 +1,426 @@
|
||||
# MangaReader Test Suite
|
||||
|
||||
Suite completa de tests para el proyecto MangaReader usando XCTest.
|
||||
|
||||
## Tabla de Contenidos
|
||||
|
||||
- [Descripción General](#descripción-general)
|
||||
- [Estructura de Tests](#estructura-de-tests)
|
||||
- [Ejecutar Tests](#ejecutar-tests)
|
||||
- [Guía de Tests](#guía-de-tests)
|
||||
- [Mejores Prácticas](#mejores-prácticas)
|
||||
|
||||
## Descripción General
|
||||
|
||||
Esta suite de tests cubre todos los componentes principales del proyecto MangaReader:
|
||||
|
||||
1. **Modelos de Datos** - Validación de Codable, edge cases, y lógica de negocio
|
||||
2. **StorageService** - Almacenamiento local, favoritos, progreso de lectura
|
||||
3. **ManhwaWebScraper** - Web scraping y parsing de HTML/JavaScript
|
||||
4. **Integración** - Flujos completos que conectan múltiples componentes
|
||||
|
||||
## Estructura de Tests
|
||||
|
||||
```
|
||||
Tests/
|
||||
├── ModelTests.swift # Tests para modelos de datos
|
||||
├── StorageServiceTests.swift # Tests para servicio de almacenamiento
|
||||
├── ManhwaWebScraperTests.swift # Tests para web scraper
|
||||
├── IntegrationTests.swift # Tests de integración
|
||||
├── TestHelpers.swift # Helpers y factories para tests
|
||||
└── XCTestSuiteExtensions.swift # Extensiones de XCTest
|
||||
```
|
||||
|
||||
## Ejecutar Tests
|
||||
|
||||
### Desde Xcode
|
||||
|
||||
1. Abrir el proyecto en Xcode
|
||||
2. Cmd + U para ejecutar todos los tests
|
||||
3. Cmd + 6 para abrir el Test Navigator
|
||||
4. Click derecho en un test específico para ejecutarlo
|
||||
|
||||
### Desde Línea de Comandos
|
||||
|
||||
```bash
|
||||
# Ejecutar todos los tests
|
||||
xcodebuild test -scheme MangaReader -destination 'platform=iOS Simulator,name=iPhone 15'
|
||||
|
||||
# Ejecutar tests específicos
|
||||
xcodebuild test -scheme MangaReader -only-testing:MangaReaderTests/ModelTests
|
||||
|
||||
# Ejecutar con cobertura
|
||||
xcodebuild test -scheme MangaReader -enableCodeCoverage YES
|
||||
```
|
||||
|
||||
## Guía de Tests
|
||||
|
||||
### ModelTests.swift
|
||||
|
||||
Prueba todos los modelos de datos del proyecto.
|
||||
|
||||
#### Tests Incluidos:
|
||||
|
||||
**Manga Model:**
|
||||
- `testMangaInitialization` - Verifica inicialización correcta
|
||||
- `testMangaCodableSerialization` - Prueba encoding/decoding JSON
|
||||
- `testMangaDisplayStatus` - Verifica traducción de estados
|
||||
- `testMangaHashable` - Prueba conformidad con Hashable
|
||||
|
||||
**Chapter Model:**
|
||||
- `testChapterInitialization` - Inicialización con valores por defecto
|
||||
- `testChapterDisplayNumber` - Formato de número de capítulo
|
||||
- `testChapterProgress` - Cálculo de progreso de lectura
|
||||
- `testChapterCodableSerialization` - Serialización JSON
|
||||
|
||||
**MangaPage Model:**
|
||||
- `testMangaPageInitialization` - Creación de páginas
|
||||
- `testMangaPageThumbnailURL` - URLs de thumbnails
|
||||
- `testMangaPageCodableSerialization` - Serialización
|
||||
|
||||
**ReadingProgress Model:**
|
||||
- `testReadingProgressInitialization` - Creación de progreso
|
||||
- `testReadingProgressIsCompleted` - Lógica de completación
|
||||
- `testReadingProgressCodableSerialization` - Persistencia
|
||||
|
||||
**DownloadedChapter Model:**
|
||||
- `testDownloadedChapterInitialization` - Creación de capítulos descargados
|
||||
- `testDownloadedChapterDisplayTitle` - Formato de títulos
|
||||
- `testDownloadedChapterCodableSerialization` - Serialización completa
|
||||
|
||||
**Edge Cases:**
|
||||
- `testMangaWithEmptyGenres` - Manejo de arrays vacíos
|
||||
- `testMangaWithNilCoverImage` - Imagen de portada opcional
|
||||
- `testChapterWithZeroNumber` - Capítulo cero
|
||||
- `testMangaPageWithNegativeIndex` - Índices negativos
|
||||
|
||||
### StorageServiceTests.swift
|
||||
|
||||
Prueba el servicio de almacenamiento local.
|
||||
|
||||
#### Tests Incluidos:
|
||||
|
||||
**Favorites:**
|
||||
- `testSaveFavorite` - Guardar un favorito
|
||||
- `testSaveMultipleFavorites` - Guardar varios favoritos
|
||||
- `testSaveDuplicateFavorite` - Evitar duplicados
|
||||
- `testRemoveFavorite` - Eliminar favorito
|
||||
- `testIsFavorite` - Verificar si es favorito
|
||||
|
||||
**Reading Progress:**
|
||||
- `testSaveReadingProgress` - Guardar progreso
|
||||
- `testSaveMultipleReadingProgress` - Múltiples progresos
|
||||
- `testUpdateExistingReadingProgress` - Actualizar progreso
|
||||
- `testGetLastReadChapter` - Obtener último capítulo leído
|
||||
- `testGetReadingProgressWhenNotExists` - Progreso inexistente
|
||||
|
||||
**Downloaded Chapters:**
|
||||
- `testSaveDownloadedChapter` - Guardar metadatos de capítulo
|
||||
- `testIsChapterDownloaded` - Verificar descarga
|
||||
- `testGetDownloadedChapters` - Listar capítulos
|
||||
- `testDeleteDownloadedChapter` - Eliminar capítulo
|
||||
|
||||
**Image Caching:**
|
||||
- `testSaveAndLoadImage` - Guardar y cargar imagen
|
||||
- `testLoadNonExistentImage` - Imagen inexistente
|
||||
- `testGetImageURL` - Obtener URL de imagen
|
||||
|
||||
**Storage Management:**
|
||||
- `testGetStorageSize` - Calcular tamaño usado
|
||||
- `testClearAllDownloads` - Limpiar todo el almacenamiento
|
||||
- `testFormatFileSize` - Formatear tamaño a legible
|
||||
|
||||
**Concurrent Operations:**
|
||||
- `testConcurrentImageSave` - Guardar imágenes concurrentemente
|
||||
|
||||
### ManhwaWebScraperTests.swift
|
||||
|
||||
Prueba el web scraper con mocks de WKWebView.
|
||||
|
||||
#### Tests Incluidos:
|
||||
|
||||
**Error Handling:**
|
||||
- `testScrapingErrorDescriptions` - Descripciones de errores
|
||||
- `testScrapingErrorLocalizedError` - Conformidad con LocalizedError
|
||||
|
||||
**Chapter Parsing:**
|
||||
- `testChapterParsingFromJavaScriptResult` - Parsear respuesta JS
|
||||
- `testChapterParsingWithInvalidData` - Manejar datos inválidos
|
||||
- `testChapterDeduplication` - Eliminar capítulos duplicados
|
||||
- `testChapterSorting` - Ordenar capítulos
|
||||
|
||||
**Image Parsing:**
|
||||
- `testImageParsingFromJavaScriptResult` - Parsear URLs de imágenes
|
||||
- `testImageParsingWithEmptyArray` - Array vacío de imágenes
|
||||
- `testImageParsingWithInvalidURLs` - Filtrar URLs inválidas
|
||||
|
||||
**Manga Info Parsing:**
|
||||
- `testMangaInfoParsingFromJavaScriptResult` - Extraer info de manga
|
||||
- `testMangaInfoParsingWithEmptyFields` - Campos vacíos
|
||||
- `testMangaStatusParsing` - Normalizar estados
|
||||
|
||||
**URL Construction:**
|
||||
- `testMangaURLConstruction` - Construir URLs de manga
|
||||
- `testChapterURLConstruction` - Construir URLs de capítulo
|
||||
- `testURLConstructionWithSpecialCharacters` - Caracteres especiales
|
||||
|
||||
**Edge Cases:**
|
||||
- `testChapterNumberExtraction` - Extraer números de capítulo
|
||||
- `testChapterSlugExtraction` - Extraer slugs
|
||||
- `testDuplicateRemovalPreservingOrder` - Eliminar duplicados manteniendo orden
|
||||
|
||||
### IntegrationTests.swift
|
||||
|
||||
Prueba flujos completos que integran múltiples componentes.
|
||||
|
||||
#### Tests Incluidos:
|
||||
|
||||
**Complete Flow:**
|
||||
- `testCompleteScrapingAndStorageFlow` - Scraper -> Storage
|
||||
- `testChapterDownloadFlow` - Descarga completa de capítulo
|
||||
- `testReadingProgressTrackingFlow` - Seguimiento de lectura
|
||||
|
||||
**Multi-Manga Scenarios:**
|
||||
- `testMultipleMangasProgressTracking` - Varios mangas
|
||||
- `testMultipleChapterDownloads` - Descargas de múltiples capítulos
|
||||
|
||||
**Error Handling:**
|
||||
- `testDownloadFlowWithMissingImages` - Imágenes faltantes
|
||||
- `testStorageCleanupFlow` - Limpieza de almacenamiento
|
||||
|
||||
**Data Persistence:**
|
||||
- `testDataPersistenceAcrossOperations` - Persistencia de datos
|
||||
|
||||
**Concurrent Operations:**
|
||||
- `testConcurrentFavoriteOperations` - Operaciones concurrentes favoritos
|
||||
- `testConcurrentProgressOperations` - Operaciones concurrentes progreso
|
||||
- `testConcurrentImageOperations` - Guardado concurrente de imágenes
|
||||
|
||||
**Large Scale:**
|
||||
- `testLargeScaleFavoriteOperations` - 1000 favoritos
|
||||
- `testLargeScaleProgressOperations` - 500 progresos
|
||||
|
||||
## TestHelpers.swift
|
||||
|
||||
Proporciona helpers y factories para crear datos de prueba:
|
||||
|
||||
### TestDataFactory
|
||||
|
||||
Crea objetos de prueba:
|
||||
|
||||
```swift
|
||||
let manga = TestDataFactory.createManga(
|
||||
slug: "test-manga",
|
||||
title: "Test Manga"
|
||||
)
|
||||
|
||||
let chapter = TestDataFactory.createChapter(number: 1)
|
||||
|
||||
let chapters = TestDataFactory.createChapters(count: 10)
|
||||
```
|
||||
|
||||
### ImageTestHelpers
|
||||
|
||||
Crea imágenes de prueba:
|
||||
|
||||
```swift
|
||||
let image = ImageTestHelpers.createTestImage(
|
||||
color: .blue,
|
||||
size: CGSize(width: 800, height: 1200)
|
||||
)
|
||||
```
|
||||
|
||||
### FileSystemTestHelpers
|
||||
|
||||
Operaciones de sistema de archivos:
|
||||
|
||||
```swift
|
||||
let tempDir = try FileSystemTestHelpers.createTemporaryDirectory()
|
||||
|
||||
try FileSystemTestHelpers.createTestChapterStructure(
|
||||
mangaSlug: "test",
|
||||
chapterNumber: 1,
|
||||
pageCount: 10,
|
||||
in: tempDir
|
||||
)
|
||||
```
|
||||
|
||||
### StorageTestHelpers
|
||||
|
||||
Limpieza y preparación de almacenamiento:
|
||||
|
||||
```swift
|
||||
StorageTestHelpers.clearAllStorage()
|
||||
|
||||
StorageTestHelpers.seedTestData(
|
||||
favoriteCount: 5,
|
||||
progressCount: 10
|
||||
)
|
||||
```
|
||||
|
||||
## Mejores Prácticas
|
||||
|
||||
### 1. Independencia de Tests
|
||||
|
||||
Cada test debe ser independiente y poder ejecutarse solo:
|
||||
|
||||
```swift
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
// Limpiar estado antes del test
|
||||
UserDefaults.standard.removeObject(forKey: "favoritesKey")
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Limpiar estado después del test
|
||||
super.tearDown()
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Nombres Descriptivos
|
||||
|
||||
Usa nombres que describan qué se está probando:
|
||||
|
||||
```swift
|
||||
// ✅ Bueno
|
||||
func testSaveDuplicateFavoriteDoesNotAddDuplicate()
|
||||
|
||||
// ❌ Malo
|
||||
func testFavorite()
|
||||
```
|
||||
|
||||
### 3. Un Assert por Test
|
||||
|
||||
Cuando sea posible, usa un assert por test:
|
||||
|
||||
```swift
|
||||
// ✅ Bueno
|
||||
func testFavoriteIsSaved() {
|
||||
storageService.saveFavorite(mangaSlug: "test")
|
||||
XCTAssertTrue(storageService.isFavorite(mangaSlug: "test"))
|
||||
}
|
||||
|
||||
func testFavoriteIsRemoved() {
|
||||
storageService.saveFavorite(mangaSlug: "test")
|
||||
storageService.removeFavorite(mangaSlug: "test")
|
||||
XCTAssertFalse(storageService.isFavorite(mangaSlug: "test"))
|
||||
}
|
||||
|
||||
// ❌ Evitar
|
||||
func testFavoriteOperations() {
|
||||
storageService.saveFavorite(mangaSlug: "test")
|
||||
XCTAssertTrue(storageService.isFavorite(mangaSlug: "test"))
|
||||
|
||||
storageService.removeFavorite(mangaSlug: "test")
|
||||
XCTAssertFalse(storageService.isFavorite(mangaSlug: "test"))
|
||||
}
|
||||
```
|
||||
|
||||
### 4. AAA Pattern
|
||||
|
||||
Usa el patrón Arrange-Act-Assert:
|
||||
|
||||
```swift
|
||||
func testChapterProgressCalculation() {
|
||||
// Arrange - Preparar el test
|
||||
var chapter = Chapter(number: 1, title: "Test", url: "", slug: "")
|
||||
let expectedPage = 5
|
||||
|
||||
// Act - Ejecutar la acción
|
||||
chapter.lastReadPage = expectedPage
|
||||
|
||||
// Assert - Verificar el resultado
|
||||
XCTAssertEqual(chapter.progress, Double(expectedPage))
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Mock de Dependencias
|
||||
|
||||
No hagas llamadas de red reales en tests unitarios:
|
||||
|
||||
```swift
|
||||
// ✅ Bueno - Mock
|
||||
let mockJSResult = [["number": 10, "title": "Chapter 10"]]
|
||||
let chapters = parseChaptersFromJS(mockJSResult)
|
||||
|
||||
// ❌ Malo - Llamada real
|
||||
let chapters = await scraper.scrapeChapters(mangaSlug: "test")
|
||||
```
|
||||
|
||||
### 6. Tests Asíncronos
|
||||
|
||||
Usa `async/await` apropiadamente:
|
||||
|
||||
```swift
|
||||
func testAsyncImageSave() async throws {
|
||||
let image = createTestImage()
|
||||
|
||||
let url = try await storageService.saveImage(
|
||||
image,
|
||||
mangaSlug: "test",
|
||||
chapterNumber: 1,
|
||||
pageIndex: 0
|
||||
)
|
||||
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
|
||||
}
|
||||
```
|
||||
|
||||
## Cobertura de Código
|
||||
|
||||
Objetivos de cobertura:
|
||||
|
||||
- **Modelos**: 95%+ (lógica crítica de datos)
|
||||
- **StorageService**: 90%+ (manejo de archivos y persistencia)
|
||||
- **Scraper**: 85%+ (con mocks de WKWebView)
|
||||
- **Integración**: 80%+ (flujos críticos de usuario)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tests Fallan Intermittentemente
|
||||
|
||||
Si un test falla solo algunas veces:
|
||||
|
||||
1. Verifica que hay cleanup adecuado en `tearDown()`
|
||||
2. Asegura que los tests son independientes
|
||||
3. Usa `waitFor` apropiadamente para operaciones asíncronas
|
||||
|
||||
### Tests de Performance Fallan
|
||||
|
||||
Si los tests de rendimiento fallan en diferentes máquinas:
|
||||
|
||||
1. Ajusta las métricas según el hardware
|
||||
2. Usa medidas relativas en lugar de absolutas
|
||||
3. Considera deshabilitar tests de performance en CI
|
||||
|
||||
### Memory Leaks en Tests
|
||||
|
||||
Para detectar memory leaks:
|
||||
|
||||
```swift
|
||||
func testNoMemoryLeak() {
|
||||
let instance = MyClass()
|
||||
assertNoMemoryLeak(instance)
|
||||
}
|
||||
```
|
||||
|
||||
## Recursos Adicionales
|
||||
|
||||
- [XCTest Documentation](https://developer.apple.com/documentation/xctest)
|
||||
- [Testing with Xcode](https://developer.apple.com/documentation/xcode/testing)
|
||||
- [Unit Testing Best Practices](https://www.objc.io/books/unit-testing/)
|
||||
|
||||
## Contribuir
|
||||
|
||||
Para agregar nuevos tests:
|
||||
|
||||
1. Decide si es unit test, integration test, o performance test
|
||||
2. Agrega el test al archivo apropiado
|
||||
3. Usa los helpers en `TestHelpers.swift` cuando sea posible
|
||||
4. Asegura que el test es independiente
|
||||
5. Agrega documentación si el test es complejo
|
||||
6. Ejecuta todos los tests para asegurar que nada se rompe
|
||||
|
||||
## Licencia
|
||||
|
||||
Mismo que el proyecto principal.
|
||||
Reference in New Issue
Block a user