Inteligencia Artificial

Genera Tests con IA Local y Segura

Echo Code

Aprende a generar pruebas en Jest y Vitest usando modelos open-source, Python y la terminal Linux. Automatiza sin exponer código a APIs externas.

Pitón de sangre viviendo su vida normalmente.

El Riesgo Actual#

El ecosistema actual de desarrollo está obsesionado con delegar responsabilidades a modelos de lenguaje masivos (LLMs) alojados en la nube. Herramientas como GitHub Copilot o ChatGPT ofrecen comodidades innegables, pero introducen un vector de riesgo crítico en entornos corporativos: la filtración de propiedad intelectual. Enviar lógica de negocio patentada a APIs de terceros para generar simples pruebas unitarias es, en el mejor de los casos, una negligencia de seguridad; en el peor, una violación de cumplimiento normativo (GDPR, HIPAA, SOC2).

Como ingenieros de software, necesitamos herramientas que se adapten a nuestros flujos de trabajo locales, operen sin conexión y ofrezcan resultados deterministas. La solución pragmática reside en combinar el poder del scripting en Python, modelos open-source cuantizados ejecutados localmente y las herramientas estándar de nuestra terminal Linux.

En este artículo, construiremos desde cero un motor de generación de tests unitarios de caja blanca y caja negra. Usaremos Python para parsear componentes TypeScript/JavaScript, extraeremos su estructura y alimentaremos un LLM local (como Llama 3 o Mistral vía Ollama) para generar suites de pruebas en Jest o Vitest. El objetivo: cero exposición de código, integración total con tu CLI y un enfoque implacable en la cobertura de edge cases.

Arquitectura del Generador Local#

Para mantener el sistema rápido, seguro y desacoplado, diseñaremos una arquitectura de tubería (pipeline) simple pero robusta. No dependeremos de extensiones de IDEs pesadas. Todo se ejecutará desde tu emulador de terminal favorito (Alacritty, Kitty, o tmux).

El flujo de ejecución consta de cuatro etapas:

  1. Ingesta y Parseo: Un script de Python recibe la ruta de un archivo .ts, .tsx, .js o .jsx como argumento desde la terminal. Utiliza expresiones regulares avanzadas y manipulación de cadenas para identificar las funciones exportadas, las interfaces (props) y las dependencias.
  2. Construcción del Contexto (Prompt Engineering): El script ensambla un system prompt estricto. Este prompt instruye al modelo a actuar como un SDET (Software Development Engineer in Test) senior, forzándolo a utilizar Jest/Vitest, aplicar metodologías de Testing Library si detecta componentes UI, y enfocarse en boundary testing (valores nulos, strings vacíos, desbordamientos numéricos, y promesas rechazadas).
  3. Inferencia Local: Mediante peticiones HTTP a la API REST de un motor local (Ollama), enviamos el contexto. Todo el procesamiento ocurre en la GPU/CPU de tu máquina de desarrollo. No hay tráfico de red saliente.
  4. Escritura y Formateo: La respuesta, que debe ser obligatoriamente código puro sin explicaciones en Markdown, se captura, se formatea y se escribe en un archivo adyacente (ej. componente.test.ts).

Requisitos del Sistema y Tooling#

Para implementar esta solución, tu entorno Linux (o WSL2) debe estar preparado. Asumimos que ya tienes Node.js y Jest/Vitest configurados en tu proyecto frontend/backend.

Para la capa de Inteligencia Artificial, utilizaremos Ollama, el estándar de facto para ejecutar LLMs en local con una sobrecarga mínima.

  1. Instalación de Ollama: Ejecuta en tu terminal el script oficial: curl -fsSL https://ollama.com/install.sh | sh.
  2. Descarga del Modelo: Para la generación de código, los modelos especializados en programación de la familia Llama 3 (o variantes como CodeQwen o Phind-CodeLlama) son ideales. En tu terminal, ejecuta: ollama run llama3:8b-instruct-q5_K_M. Usamos una versión cuantizada para que corra fluidamente incluso si solo dispones de 8GB o 16GB de RAM.
  3. Entorno Python: Necesitaremos Python 3.10+ y la librería requests.
Terminal window
# Configuración del entorno virtual
python3 -m venv test_env
source test_env/bin/activate
pip install requests

Fase 1: Parseo de Componentes en Python#

Enviar todo un archivo de 1000 líneas a un LLM local puede saturar su ventana de contexto (típicamente entre 4K y 8K tokens para modelos pequeños) y degradar la calidad de la respuesta. El pragmatismo dicta que aislemos lo importante.

Aunque en entornos altamente complejos usaríamos bindings de tree-sitter para generar un Abstract Syntax Tree (AST), para este artículo construiremos un parser ligero y eficiente usando las librerías estándar de Python. Nuestro objetivo es extraer las firmas de las funciones, los tipos exportados y limpiar el ruido.

parser.py
import re
def extract_metadata(file_content: str) -> dict:
"""
Extrae firmas de funciones exportadas e interfaces de un archivo JS/TS.
"""
metadata = {
"imports": [],
"exported_functions": [],
"interfaces": [],
"raw_content": file_content # Fallback
}
# Capturar imports para entender dependencias
import_pattern = re.compile(r'^import\s+.*?;', re.MULTILINE)
metadata["imports"] = import_pattern.findall(file_content)
# Capturar funciones exportadas (arrow functions y funciones estándar)
export_func_pattern = re.compile(r'export\s+(?:const|function|async function)\s+([a-zA-Z0-9_]+)\s*(?:=|)(?:\s*\([^)]*\))')
metadata["exported_functions"] = export_func_pattern.findall(file_content)
# Capturar interfaces de TypeScript (útil para componentes UI)
interface_pattern = re.compile(r'export\s+interface\s+[a-zA-Z0-9_]+\s*\{[^}]*\}', re.MULTILINE)
metadata["interfaces"] = interface_pattern.findall(file_content)
return metadata

Este enfoque reduce radicalmente el tamaño del payload. Para funciones matemáticas puras o utilidades, enviamos solo la firma y dejamos que el LLM deduzca las pruebas de caja negra. Sin embargo, para componentes complejos o pruebas de caja blanca, enviaremos el archivo completo, sabiendo que estamos operando en una red air-gapped o estrictamente local.

Fase 2: Ingeniería de Prompts para Edge Cases#

El fracaso más común al generar pruebas con IA es obtener aserciones triviales (expect(true).toBe(true)). Para evitar esto, el System Prompt debe ser imperativo y arquitectónicamente rígido.

Debemos instruir al modelo para que:

  1. Utilice describe, it y convenciones modernas.
  2. Haga mocking exhaustivo de dependencias externas (llamadas a red, accesos al DOM, timers).
  3. Cubra Happy Path, Unhappy Path y Edge Cases extremos.
  4. Omita absolutamente cualquier texto conversacional.

Aquí es donde nuestro script de Python ensambla el comportamiento:

prompt_engine.py
def build_system_prompt(framework="vitest") -> str:
return f"""Eres un Ingeniero de Software Senior especializado en QA y Testing.
Tu única tarea es escribir tests unitarios de calidad de producción en TypeScript usando {framework}.
REGLAS ESTRICTAS:
1. NO escribas introducciones, conclusiones ni texto en markdown. SOLO CÓDIGO.
2. Utiliza la sintaxis de {framework} (`describe`, `it`, `expect`, `vi.fn()` o `jest.fn()`).
3. Si estás probando un componente de UI, utiliza `@testing-library/react` o `@testing-library/vue`.
4. Debes incluir al menos un bloque `describe('Edge cases', ...)` que pruebe:
- Parámetros nulos o indefinidos.
- Arrays/Strings vacíos.
- Comportamiento asíncrono con promesas rechazadas si aplica.
5. Haz mock de todos los módulos importados excepto el módulo bajo prueba.
"""
def build_user_prompt(filename: str, metadata: dict) -> str:
return f"""Analiza el archivo {filename}.
Aquí tienes las dependencias e interfaces detectadas:
{chr(10).join(metadata['imports'])}
{chr(10).join(metadata['interfaces'])}
Aquí está el código fuente del componente/utilidad:
{metadata['raw_content']}
Escribe el archivo de prueba completo ahora.
"""

Fase 3: Integración y Ejecución con Ollama#

Ahora uniremos las piezas. Usaremos la API REST de Ollama (http://localhost:11434/api/generate) porque no bloquea el hilo de ejecución y permite el streaming de tokens si decidimos implementar retroalimentación visual en la terminal en el futuro. Por ahora, haremos una llamada síncrona para mayor simplicidad en el CI/CD.

A continuación, el núcleo del script de ejecución. Este archivo (generate_tests.py) es el punto de entrada que invocaremos desde nuestra terminal Linux.

generate_tests.py
import sys
import os
import json
import requests
import argparse
from parser import extract_metadata
from prompt_engine import build_system_prompt, build_user_prompt
OLLAMA_ENDPOINT = "http://localhost:11434/api/generate"
MODEL_NAME = "llama3:8b-instruct-q5_K_M"
def generate_test_for_file(filepath: str, framework: str):
if not os.path.exists(filepath):
print(f"[ERROR] Archivo no encontrado: {filepath}")
sys.exit(1)
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
filename = os.path.basename(filepath)
metadata = extract_metadata(content)
system_prompt = build_system_prompt(framework)
user_prompt = build_user_prompt(filename, metadata)
payload = {
"model": MODEL_NAME,
"prompt": user_prompt,
"system": system_prompt,
"stream": False,
"options": {
"temperature": 0.2, # Baja temperatura para código determinista
"top_p": 0.9,
"num_predict": 1500
}
}
print(f"[*] Analizando {filename} con {MODEL_NAME}...")
try:
response = requests.post(OLLAMA_ENDPOINT, json=payload)
response.raise_for_status()
data = response.json()
# Limpiar bloques de markdown si el modelo desobedece
test_code = data.get("response", "")
test_code = test_code.replace("```typescript", "").replace("```javascript", "").replace("```", "").strip()
# Generar ruta del archivo de test
dir_name = os.path.dirname(filepath)
base_name, ext = os.path.splitext(filename)
test_filename = f"{base_name}.test{ext}"
test_filepath = os.path.join(dir_name, test_filename)
with open(test_filepath, 'w', encoding='utf-8') as f:
f.write(test_code)
print(f"[+] Test generado con éxito: {test_filepath}")
except requests.exceptions.RequestException as e:
print(f"[ERROR] Falló la comunicación con Ollama: {e}")
print("Asegúrate de que Ollama está corriendo (systemctl start ollama o ollama serve).")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generador de Tests Unitarios con IA Local")
parser.add_argument("file", help="Ruta al archivo fuente (.js, .ts, .tsx)")
parser.add_argument("--framework", choices=['jest', 'vitest'], default='vitest', help="Framework de testing")
args = parser.parse_args()
generate_test_for_file(args.file, args.framework)

Ejecución desde la Terminal Linux#

La verdadera potencia de esta herramienta de línea de comandos (CLI) construida en Python se revela cuando la combinamos con las utilidades de procesamiento de texto de Unix.

Si tienes un directorio de utilidades sin cobertura de pruebas, puedes generar suites para todos los archivos .ts en un solo comando usando find y xargs. Abre tu terminal y ejecuta:

Terminal window
# Ejecución para un solo archivo
python3 generate_tests.py src/utils/formatCurrency.ts --framework vitest
# Generación masiva (batch processing) para un directorio entero
find ./src/utils -type f -name "*.ts" ! -name "*.test.ts" | xargs -I {} python3 generate_tests.py {} --framework vitest

En cuestión de segundos o minutos (dependiendo de la VRAM de tu GPU o la potencia de tu CPU), tu directorio comenzará a poblarse con archivos *.test.ts. Todo esto sucede sin enviar un solo byte de información fuera de tu red de área local. Cero exposición. Privacidad absoluta de la lógica de negocio corporativa.

Análisis del Resultado: Manejo de Edge Cases#

Supongamos que pasamos un archivo de utilidad simple que procesa pagos. El modelo local, restringido por nuestra baja temperatura (0.2) y nuestras instrucciones estrictas en el prompt, no generará pruebas superficiales.

El output generado lucirá similar a esto:

import { describe, it, expect, vi } from 'vitest';
import { processPayment } from './paymentProcessor';
import { apiService } from '../services/api';
// Mock del servicio externo
vi.mock('../services/api', () => ({
apiService: {
postTransaction: vi.fn()
}
}));
describe('processPayment', () => {
it('debería procesar el pago correctamente (Happy Path)', async () => {
vi.mocked(apiService.postTransaction).mockResolvedValue({ status: 200 });
const result = await processPayment(150.50, 'USD');
expect(result.success).toBe(true);
expect(apiService.postTransaction).toHaveBeenCalledWith(150.50, 'USD');
});
describe('Edge cases', () => {
it('debería lanzar un error si el monto es negativo o cero', async () => {
await expect(processPayment(-10, 'USD')).rejects.toThrow('Invalid amount');
await expect(processPayment(0, 'USD')).rejects.toThrow('Invalid amount');
});
it('debería manejar fallos de red desde el API (Unhappy Path)', async () => {
vi.mocked(apiService.postTransaction).mockRejectedValue(new Error('Network timeout'));
const result = await processPayment(100, 'EUR');
expect(result.success).toBe(false);
expect(result.error).toBe('Network timeout');
});
it('debería fallar si los parámetros son nulos o malformados', async () => {
// @ts-expect-error probando input inválido en runtime
await expect(processPayment(null, 'USD')).rejects.toThrow();
});
});
});

El modelo ha utilizado vi.mock de manera nativa (específico de Vitest, como se le instruyó), ha interceptado dependencias de red, y ha construido bloques aislados para valores extremos (-10, 0, null) basándose en nuestra exigencia explícita en el System Prompt.

Refinando la Herramienta#

Este motor base es solo el principio. Al ejecutar estos scripts desde tu terminal Linux, las posibilidades de integración continua se multiplican. Puedes enganchar este script de Python a un Git Pre-commit Hook (usando Husky, por ejemplo) para analizar archivos en el staging area y advertirte si estás haciendo un commit de un componente sin su correspondiente archivo de prueba, generando un borrador automáticamente.

Otra mejora sustancial para el parser sería implementar verificación de tipos antes de enviar el prompt. Si el script de Python detecta que el archivo .ts importa módulos pesados de React (useState, useEffect), puede inyectar dinámicamente un bloque adicional en el prompt instruyendo al modelo a renderizar mocks del DOM usando @testing-library/react.

Conclusión#

El uso de Inteligencia Artificial en el ciclo de vida del desarrollo de software no requiere comprometer la seguridad ni la privacidad del código fuente corporativo. Mediante el pragmatismo de los modelos open-source cuantizados, la flexibilidad del scripting en Python y el poder histórico de la terminal de Linux, hemos construido una tubería que automatiza el trabajo tedioso del setup de Jest/Vitest.

La generación local garantiza que eres dueño absoluto de tu infraestructura. Los tests unitarios no deberían ser una carga cognitiva ni un riesgo de seguridad; automatizar su esqueleto y casos borde con IA local te permite, como ingeniero senior, dedicar tu tiempo a la arquitectura, el rendimiento y los problemas que realmente requieren intuición humana.