initial commit

This commit is contained in:
2025-10-26 23:39:49 -05:00
commit 5fb0909e8d
120 changed files with 11279 additions and 0 deletions

138
tests/README.md Normal file
View File

@@ -0,0 +1,138 @@
# 🧪 Tests del Balotario Licencia A-I
Este directorio contiene tests automatizados para verificar el funcionamiento correcto de la aplicación Flask.
## 📁 Archivos de Test
### 🐍 Tests Automatizados con pytest
#### `test_app.py`
- **Propósito**: Tests completos de la aplicación Flask
- **Verifica**:
- Páginas web (index, study, practice, exam)
- API endpoints (/api/questions)
- Estructura de datos de preguntas
- Archivos estáticos (CSS, JS, favicon)
#### `test_parser.py`
- **Propósito**: Tests del parser de markdown
- **Verifica**:
- Parsing correcto de preguntas, opciones y respuestas
- Soporte para imágenes locales y remotas
- Estructura de datos válida
#### `test_clean_parser.py`
- **Propósito**: Tests de limpieza de emojis ✅
- **Verifica**: Eliminación correcta de marcadores visuales
#### `conftest.py`
- **Propósito**: Configuración global de pytest
- **Contiene**: Fixtures para cliente Flask y configuración de tests
## 🚀 Cómo Ejecutar los Tests
### Opción 1: Usando Makefile (Recomendado)
```bash
# Ejecutar todos los tests con cobertura
make test
# Instalar dependencias de desarrollo
make install-dev
```
### Opción 2: Usando pytest directamente
```bash
# Activar entorno virtual
source venv/bin/activate # Linux/Mac
# o
venv\Scripts\activate # Windows
# Ejecutar todos los tests
pytest
# Con cobertura detallada
pytest --cov=. --cov-report=html --cov-report=term-missing
# Ejecutar tests específicos
pytest tests/test_app.py
pytest tests/test_parser.py
```
### Ver Reporte de Cobertura
```bash
# Abrir reporte HTML (se genera automáticamente)
open htmlcov/index.html # Mac
xdg-open htmlcov/index.html # Linux
start htmlcov/index.html # Windows
```
## 📊 Cobertura de Tests
### ✅ Funcionalidades Probadas (15 tests)
- [x] **Páginas web**: Index, Study, Practice, Exam
- [x] **API endpoints**: /api/questions con diferentes modos
- [x] **Parser de markdown**: Preguntas, opciones, respuestas correctas
- [x] **Imágenes**: Soporte local y remoto
- [x] **Archivos estáticos**: CSS, JS, favicon
- [x] **Validación de datos**: Estructura de preguntas
- [x] **Manejo de errores**: Parámetros inválidos
- [x] **Limpieza de emojis**: Eliminación de marcadores ✅
### 📈 Estadísticas Actuales
- **Tests**: 15 pasando ✅
- **Cobertura**: ~67% del código
- **Archivos cubiertos**: app.py (82%), config.py (100%)
### 🔄 Posibles Mejoras Futuras
- [ ] Tests de JavaScript (frontend)
- [ ] Tests de integración completa
- [ ] Tests de rendimiento
- [ ] Tests de accesibilidad
- [ ] Tests de responsive design
## 🛠️ Desarrollo de Tests
### Agregar Nuevos Tests
1. Crear archivo `test_[funcionalidad].py` en el directorio `tests/`
2. Usar las fixtures de `conftest.py` (client, app)
3. Seguir el patrón de naming: `test_[descripcion]()`
4. Documentar en este README
### Convenciones
- **Naming**: `test_[funcionalidad]_[caso]()`
- **Assertions**: Usar `assert` con mensajes descriptivos
- **Fixtures**: Reutilizar `client` y `app` de conftest.py
- **Docstrings**: Describir qué verifica cada test
### Ejemplo de Test
```python
def test_new_feature(client):
"""Test that new feature works correctly."""
response = client.get('/new-endpoint')
assert response.status_code == 200
assert b'expected content' in response.data
```
## 🐛 Debugging
### Si un Test Falla
1. **Leer el mensaje de error** completo
2. **Ejecutar test individual**: `pytest tests/test_app.py::test_specific_function -v`
3. **Verificar fixtures**: Asegurar que `conftest.py` esté correcto
4. **Revisar imports**: Verificar que los módulos se importen correctamente
### Comandos Útiles
```bash
# Test específico con output detallado
pytest tests/test_app.py::test_index_page -v -s
# Parar en el primer fallo
pytest -x
# Mostrar variables locales en fallos
pytest --tb=long
```
---
**Nota**: Estos son tests automatizados que se ejecutan con pytest. Proporcionan verificación continua de que la aplicación funciona correctamente.

33
tests/conftest.py Normal file
View File

@@ -0,0 +1,33 @@
"""
Pytest configuration and fixtures
"""
import sys
import os
import pytest
# Add the parent directory to the Python path so we can import the app
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app import app as flask_app
@pytest.fixture
def app():
"""Create and configure a new app instance for each test."""
flask_app.config['TESTING'] = True
flask_app.config['WTF_CSRF_ENABLED'] = False
yield flask_app
@pytest.fixture
def client(app):
"""A test client for the app."""
return app.test_client()
@pytest.fixture
def runner(app):
"""A test runner for the app's Click commands."""
return app.test_cli_runner()

184
tests/test_app.py Normal file
View File

@@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""
Tests for the Flask application
"""
import pytest
import json
# Fixtures are imported from conftest.py
def test_index_page(client):
"""Test that the index page loads correctly."""
response = client.get('/')
assert response.status_code == 200
assert b'Balotario Licencia Clase A' in response.data
assert b'Estudiar' in response.data
assert b'Practicar' in response.data
assert b'Examen' in response.data
def test_study_page(client):
"""Test that the study page loads correctly."""
response = client.get('/study')
assert response.status_code == 200
assert b'Modo Estudio' in response.data
def test_practice_page(client):
"""Test that the practice page loads correctly."""
response = client.get('/practice')
assert response.status_code == 200
assert b'Modo Pr\xc3\xa1ctica' in response.data # "Práctica" in UTF-8
def test_exam_page(client):
"""Test that the exam page loads correctly."""
response = client.get('/exam')
assert response.status_code == 200
assert b'Examen Simulado' in response.data
def test_api_questions_all(client):
"""Test the API endpoint for getting all questions."""
response = client.get('/api/questions?mode=all')
assert response.status_code == 200
data = json.loads(response.data)
assert isinstance(data, list)
assert len(data) > 0
# Check structure of first question
if data:
question = data[0]
assert 'id' in question
assert 'question' in question
assert 'options' in question
assert 'correct' in question
assert 'has_image' in question
assert isinstance(question['options'], list)
assert len(question['options']) >= 2
def test_api_questions_range(client):
"""Test the API endpoint for getting questions by range."""
response = client.get('/api/questions?mode=range&start=1&end=5')
assert response.status_code == 200
data = json.loads(response.data)
assert isinstance(data, list)
assert len(data) <= 5
# Check that question IDs are within range
for question in data:
assert 1 <= question['id'] <= 5
def test_api_questions_random(client):
"""Test the API endpoint for getting random questions."""
response = client.get('/api/questions?mode=random&count=10')
assert response.status_code == 200
data = json.loads(response.data)
assert isinstance(data, list)
assert len(data) <= 10
def test_api_questions_invalid_mode(client):
"""Test the API endpoint with invalid mode (falls back to all questions)."""
response = client.get('/api/questions?mode=invalid')
assert response.status_code == 200
data = json.loads(response.data)
assert isinstance(data, list)
# Should return all questions as fallback
assert len(data) > 0
def test_api_questions_missing_parameters(client):
"""Test the API endpoint with missing parameters (uses defaults)."""
# Range mode without start/end (uses defaults)
response = client.get('/api/questions?mode=range')
assert response.status_code == 200
data = json.loads(response.data)
assert isinstance(data, list)
assert len(data) > 0
# Random mode without count (uses default of 20)
response = client.get('/api/questions?mode=random')
assert response.status_code == 200
data = json.loads(response.data)
assert isinstance(data, list)
assert len(data) <= 20 # Should use default count
def test_static_files(client):
"""Test that static files are accessible."""
# Test CSS file
response = client.get('/static/css/fontawesome-local.css')
assert response.status_code == 200
# Test JS file
response = client.get('/static/js/app.js')
assert response.status_code == 200
def test_favicon(client):
"""Test that favicon is accessible."""
response = client.get('/static/favicon.svg')
assert response.status_code == 200
def test_question_parsing_consistency(client):
"""Test that question parsing is consistent across API calls."""
# Get the same question range twice
response1 = client.get('/api/questions?mode=range&start=1&end=3')
response2 = client.get('/api/questions?mode=range&start=1&end=3')
assert response1.status_code == 200
assert response2.status_code == 200
data1 = json.loads(response1.data)
data2 = json.loads(response2.data)
# Should return the same questions
assert len(data1) == len(data2)
for q1, q2 in zip(data1, data2):
assert q1['id'] == q2['id']
assert q1['question'] == q2['question']
assert q1['correct'] == q2['correct']
def test_question_structure_validation(client):
"""Test that all questions have the required structure."""
response = client.get('/api/questions?mode=range&start=1&end=10')
assert response.status_code == 200
data = json.loads(response.data)
for question in data:
# Required fields
assert 'id' in question
assert 'question' in question
assert 'options' in question
assert 'correct' in question
assert 'has_image' in question
# Data types
assert isinstance(question['id'], int)
assert isinstance(question['question'], str)
assert isinstance(question['options'], list)
assert isinstance(question['correct'], str)
assert isinstance(question['has_image'], bool)
# Content validation
assert len(question['question']) > 0
assert len(question['options']) >= 2
assert question['correct'] in ['a', 'b', 'c', 'd']
# If has image, should have image URL
if question['has_image']:
assert 'image' in question
assert len(question['image']) > 0

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
import re
def test_clean_parser():
# Simular algunas líneas del markdown con ✅
test_lines = [
"a) Recoger o dejar pasajeros o carga en cualquier lugar",
"b) Dejar animales sueltos o situarlos de forma tal que obstaculicen solo un poco el tránsito",
"✅ c) Recoger o dejar pasajeros en lugares autorizados.",
"d) Ejercer el comercio ambulatorio o estacionario"
]
print("🧪 Probando el parser limpio...")
print("=" * 50)
options = []
correct_option = ""
for line in test_lines:
original_line = line.strip()
print(f"Línea original: '{original_line}'")
# Verificar si esta línea tiene el ✅
if '' in original_line:
# Extraer la letra de la opción correcta
match = re.search(r'\s*([a-d])\)', original_line)
if match:
correct_option = match.group(1)
print(f" ✅ Respuesta correcta encontrada: {correct_option}")
# Limpiar la línea removiendo el ✅ completamente
clean_line = re.sub(r'\s*', '', original_line)
clean_line = re.sub(r'', '', clean_line) # Por si hay ✅ sin espacios
clean_line = clean_line.strip()
print(f" Línea limpia: '{clean_line}'")
print(f" ¿Contiene ✅?: {'' in clean_line}")
options.append(clean_line)
print("-" * 30)
print(f"\n📊 Resultado final:")
print(f"Respuesta correcta: {correct_option}")
print(f"Opciones limpias:")
for i, option in enumerate(options):
letter = chr(97 + i) # a, b, c, d
marker = "" if letter == correct_option else " "
print(f" {marker} {option}")
# Verificar que no hay ✅ en las opciones
if '' in option:
print(f" ❌ ERROR: Todavía hay ✅ en la opción!")
else:
print(f" ✅ OK: Sin ✅ en la opción")
if __name__ == "__main__":
test_clean_parser()

112
tests/test_parser.py Normal file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env python3
import re
def test_parse_markdown_questions():
with open('data/balotario_clase_a_cat_I.md', 'r', encoding='utf-8') as file:
content = file.read()
questions = []
# Dividir el contenido por preguntas usando ### como separador
question_blocks = re.split(r'\n### (\d+)\n', content)[1:] # Ignorar el primer elemento vacío
for i in range(0, len(question_blocks), 2):
if i + 1 >= len(question_blocks):
break
question_num = int(question_blocks[i])
question_content = question_blocks[i + 1].strip()
# Solo procesar las primeras 3 preguntas para prueba
if question_num > 3:
break
# Verificar si hay imagen al inicio (ahora soporta imágenes locales)
has_image = ('![](https://sierdgtt.mtc.gob.pe/Content/img-data/' in question_content or
'![](/static/images/' in question_content)
image_url = ""
if has_image:
# Intentar primero imágenes locales, luego remotas
img_match = re.search(r'!\[\]\((/static/images/[^)]+)\)', question_content)
if not img_match:
img_match = re.search(r'!\[\]\((https://sierdgtt\.mtc\.gob\.pe/Content/img-data/[^)]+)\)', question_content)
if img_match:
image_url = img_match.group(1)
question_content = re.sub(r'!\[\]\([^)]+\)\n*', '', question_content).strip()
# Separar pregunta de opciones
lines = question_content.split('\n')
question_lines = []
option_lines = []
in_options = False
for line in lines:
line = line.strip()
if not line:
continue
if re.match(r'^✅?\s*[a-d]\)', line):
in_options = True
option_lines.append(line)
elif not in_options:
question_lines.append(line)
question_text = ' '.join(question_lines).strip()
# Extraer opciones y respuesta correcta
options = []
correct_option = ""
for line in option_lines:
original_line = line.strip()
# Verificar si esta línea tiene el ✅
if '' in original_line:
# Extraer la letra de la opción correcta
match = re.search(r'\s*([a-d])\)', original_line)
if match:
correct_option = match.group(1)
# Limpiar la línea removiendo el ✅ completamente
clean_line = re.sub(r'\s*', '', original_line).strip()
options.append(clean_line)
if len(options) >= 2 and correct_option and question_text: # Validar que tenemos datos completos
questions.append({
'id': question_num,
'question': question_text,
'options': options,
'correct': correct_option,
'image': image_url,
'has_image': has_image
})
# Assertions para validar que el parsing funciona correctamente
assert len(questions) > 0, "No se parsearon preguntas"
assert len(questions) <= 3, "Se parsearon más preguntas de las esperadas"
for q in questions:
assert 'id' in q, "Falta el ID de la pregunta"
assert 'question' in q, "Falta el texto de la pregunta"
assert 'options' in q, "Faltan las opciones"
assert 'correct' in q, "Falta la respuesta correcta"
assert len(q['options']) >= 2, f"Pregunta {q['id']} tiene menos de 2 opciones"
assert q['correct'] in ['a', 'b', 'c', 'd'], f"Respuesta correcta inválida en pregunta {q['id']}"
assert len(q['question']) > 0, f"Pregunta {q['id']} está vacía"
# Ejecutar prueba
if __name__ == "__main__":
questions = test_parse_markdown_questions()
print(f"✅ Se parsearon {len(questions)} preguntas correctamente")
print("=" * 60)
for q in questions:
print(f"Pregunta {q['id']}: {q['question'][:50]}...")
print(f"Respuesta correcta: {q['correct']}")
print("Opciones:")
for i, option in enumerate(q['options']):
letter = chr(97 + i) # a, b, c, d
marker = "" if letter == q['correct'] else " "
print(f" {marker} {option}")
print("-" * 40)