Probablemente estés en una de estas dos situaciones: o tu proyecto de JavaScript casi no tiene tests y cada refactor es un riesgo, o ya tienes tests pero la mitad son lentos, frágiles y sorprendentemente poco fiables.
Eso empeora en Capacitor y Electron apps. Una funcionalidad simple puede tocar lógica de negocio compartida, APIs del navegador, plugins nativos, archivos locales, IPC y servicios remotos en el mismo flujo. Si pruebas esas piezas mal, tu suite se convierte en un laberinto de dependencias falsas. Si las pruebas bien, obtienes feedback rápido sobre la lógica que falla.
El buen trabajo con unit tests en JavaScript no empieza con una sintaxis ingeniosa de matchers. Empieza con un límite disciplinado: prueba la lógica pura directamente, aísla los efectos secundarios y evita escribir tests que se rompan en cuanto renombras una función interna.
Tabla de contenidos
- Elegir tu framework de testing en JavaScript
- Configuración del proyecto y tu primer test
- Dominar los mocks y los Code asíncronos
- Estrategias avanzadas para pruebas robustas
- Pruebas para CI, Capacitor y apps de Electron
- Preguntas frecuentes sobre pruebas unitarias en JavaScript
Elegir tu framework de testing en JavaScript
Un proyecto profesional de JavaScript necesita un test runner real. Los scripts ad hoc y las comprobaciones manuales en consola no escalan cuando varios ingenieros trabajan en la misma base de código. Necesitas descubrimiento de pruebas, aserciones, manejo asíncrono, mocks y una forma de ejecutar todo de manera consistente en desarrollo local y CI.
Las recomendaciones actuales convergen en un pequeño conjunto de opciones convencionales. Jest, Mocha y Jasmine se destacan repetidamente como los frameworks principales, junto con Jest suele destacarse por su estructura de pruebas integrada, aserciones, mocking y soporte asíncrono en un solo paquete, como se muestra en este laboratorio de pruebas de JavaScript de Pluralsight.

Por qué un framework no es opcional
El primer error que cometen los equipos es tratar las pruebas unitarias como una actividad secundaria. Eso suele provocar nombres de archivo inconsistentes, aserciones personalizadas que nadie recuerda y helpers que solo una persona entiende.
Un framework te da un lenguaje compartido:
- Estructura de pruebas con
describeytestoit - Aserciones con matchers legibles
- Hooks para setup y teardown
- Soporte asíncrono para promesas y timers
- Herramientas de mocking para dependencias externas
Si tu equipo también necesita una visión más amplia de la automatización de pruebas más allá del nivel unitario, Capgo ofrece un resumen útil sobre pruebas automatizadas en flujos de entrega de apps.
Jest vs Mocha de un vistazo
Jest y Mocha representan dos filosofías distintas.
Jest es la opción todo en uno. Incluye casi todo lo que los equipos necesitan desde el primer día.
Mocha es más modular. Te da un runner y espera que armes el resto del stack.
| Característica | Jest | Mocha |
|---|---|---|
| Complejidad de setup | Menor para la mayoría de los equipos | Mayor, ya que sueles añadir librerías de aserciones y mocking |
| Aserciones | Integrado | Suele combinarse con otra librería |
| Mocking | Integrado | Suele combinarse con otra librería |
| Testing asíncrono | Integrado y sencillo | Soportado, pero depende más de la configuración circundante |
| Workflow de cobertura | Comúnmente integrado en la misma toolchain | A menudo más ensamblado a mano |
| Mejor opción | Proyectos nuevos, equipos que buscan consistencia | Stacks legacy, equipos que buscan control modular |
Regla práctica: Si tu equipo tiene que preguntarse qué librería de aserciones y de mocking combinar con el runner, probablemente quieras Jest.
Lo que recomiendo para la mayoría de los equipos
Para la mayoría de los proyectos modernos, elegiría Jest a menos que el codebase ya tenga razones de peso para seguir usando Mocha. Esta recomendación cobra más fuerza cuando la aplicación incluye Capacitor o Electron, porque esos proyectos ya tienen suficientes piezas móviles. Reducir la dispersión de herramientas de testing rinde frutos rápidamente.
Mocha sigue teniendo sentido en servicios de Node.js antiguos o bases de código longevas donde el ecosistema ya está asentado. Pero para un ingeniero de nivel medio que monta una suite robusta desde cero, Jest suele quitar más fricción de la que añade.
Una nota importante sobre el alcance. Cypress y Playwright son herramientas excelentes, pero resuelven otro problema. Van mejor para pruebas a nivel de navegador y end-to-end, no para el bucle interno rápido donde deben vivir las pruebas unitarias de JavaScript.
Configuración del proyecto y tu primer test
Un entorno de testing limpio debería ser algo aburrido. Si añadir el primer test parece complicado, es probable que la suite no se mantenga sana.

Una configuración sencilla de Jest
Empieza con un proyecto de JavaScript que ya tenga un package.json. Luego añade Jest como dependencia de desarrollo y configura un script de test.
{
"scripts": {
"test": "jest"
}
}
Eso es suficiente para muchos proyectos. Puedes añadir más configuración más adelante si tu sistema de módulos, transpilación o estructura de monorepo lo requiere.
Si estás construyendo una app Capacitor localmente y quieres tener listo tu entorno de dev antes de añadir tests a la lógica compartida, la guía de Capgo para configurar un entorno local de Capacitor es un complemento práctico.
Escribe el test antes del code
El patrón test-first no es solo una preferencia personal. La guía de JavaScript de la Consumer Financial Protection Bureau de EE. UU. recomienda explícitamente escribir la prueba primero, organizar las pruebas con describe y it, y enmarcar las comprobaciones en torno a expect(...) aserciones en su guía de pruebas unitarias en JavaScript.
Eso importa porque el enfoque test-first cambia cómo diseñas code. Las funciones tienden a ser más pequeñas, las dependencias se vuelven más visibles y los efectos secundarios dejan de filtrarse en la lógica que debería permanecer pura.
Aquí tienes un ejemplo mínimo:
// math.js
function addTax(amount, rate) {
return amount + amount * rate;
}
module.exports = { addTax };
// math.test.js
const { addTax } = require('./math');
describe('addTax', () => {
it('returns the amount with the tax applied', () => {
expect(addTax(100, 0.2)).toBe(120);
});
});
Usa siempre Arrange Act Assert
El Arrange, Act, Assert el patrón mantiene las pruebas legibles, incluso cuando se vuelven más complejas.
- Preparar la entrada y la configuración necesaria.
- Ejecutar llamando a la función.
- Verificar en el resultado.
Aplicado a un helper de validación:
function isSupportedPlatform(platform) {
return ['ios', 'android', 'web', 'desktop'].includes(platform);
}
describe('isSupportedPlatform', () => {
it('returns true for ios', () => {
// Arrange
const platform = 'ios';
// Act
const result = isSupportedPlatform(platform);
// Assert
expect(result).toBe(true);
});
});
Los tests pequeños envejecen bien. Un test debería responder a una sola pregunta, no narrar todo un flujo de trabajo.
Para proyectos de Capacitor y Electron, esa disciplina importa más porque tu lógica pura suele estar junto al code de integración nativa o de escritorio. Mantén la regla de negocio testeable sin el runtime de la plataforma, y tu primer test no será el último útil.
Dominar los mocks y los Code asíncronos
La mayoría de los bugs en el code de las aplicaciones no vienen de sumar dos números. Vienen del code que interactúa con el exterior: peticiones de red, archivos, APIs de plugins, timers, canales IPC o capas de almacenamiento.
Ahí es donde el mocking ayuda. Te da control sobre el límite para que el test pueda centrarse en la lógica de tu code.

Mockea los límites, no todo
Guía para tests mantenibles que enfatiza cobertura de comportamiento único y una fuerte aserción por test, y también advierte que abusar de mocks hace que los tests sean frágiles y acoplados a detalles de implementación, como se resume en este artículo de TestRail sobre unit tests mantenibles.
Esa advertencia es muy importante en JavaScript. Los equipos suelen empezar mockeando cada módulo importado y terminan probando si las funciones llaman a otras en el “orden correcto”, en lugar de probar el comportamiento real.
Mal objetivo para un test con muchos mocks:
- si el helper A llamó al helper B
- si el servicio C llamó al serializer D
- si una función privada interna se ejecutó dos veces
Mejor objetivo:
- lo que la función devolvió
- si manejó correctamente una dependencia fallida
- si transformó los datos en el formato esperado
Un mejor patrón para Capacitor y code de Electron
En apps móviles y de escritorio, prefiero una capa wrapper alrededor de las APIs nativas o de plataforma. Luego los unit tests mockean el wrapper, no la plataforma en sí.
Estructura de ejemplo:
// cameraGateway.js
async function getPhoto(cameraPlugin) {
return cameraPlugin.getPhoto();
}
module.exports = { getPhoto };
// profilePhotoService.js
async function loadProfilePhoto(cameraGateway) {
const photo = await cameraGateway.getPhoto();
return { path: photo.path, ready: true };
}
module.exports = { loadProfilePhoto };
// profilePhotoService.test.js
const { loadProfilePhoto } = require('./profilePhotoService');
test('returns mapped photo data', async () => {
const fakeCameraGateway = {
getPhoto: jest.fn().mockResolvedValue({ path: '/tmp/pic.jpg' })
};
const result = await loadProfilePhoto(fakeCameraGateway);
expect(result).toEqual({ path: '/tmp/pic.jpg', ready: true });
});
Ese patrón también funciona para Electron. Envuelve ipcRenderer, acceso a archivos o integraciones de shell mediante un adaptador ligero. Los unit tests prueban la capa de servicio, no el runtime directamente.
Para equipos que testean la lógica de release y rutas de actualización en apps Capacitor, Capgo tiene una guía útil sobre cómo testear actualizaciones OTA de Capacitor con escenarios mock.
Una guía rápida ayuda si tu equipo aún está normalizando su estilo de tests async:
Testear flujos async sin flakiness
Usa async/await en tests cuando el code bajo prueba devuelve una promesa. Es más claro que los patrones basados en callbacks y más fácil de debuguear.
async function fetchProfile(api) {
const response = await api.getUser();
return response.name;
}
test('returns the user name from the API response', async () => {
const api = {
getUser: jest.fn().mockResolvedValue({ name: 'Ava' })
};
const result = await fetchProfile(api);
expect(result).toBe('Ava');
});
También testea la ruta de fallo:
test('throws when the API request fails', async () => {
const api = {
getUser: jest.fn().mockRejectedValue(new Error('network failed'))
};
await expect(fetchProfile(api)).rejects.toThrow('network failed');
});
Prueba tanto el camino feliz como el feo. En producción, el feo suele ser el que los usuarios recuerdan.
Estrategias avanzadas para pruebas robustas
Un suite de pruebas resulta útil cuando sigue siendo útil después de que cambie code. Eso es más difícil que escribir un montón de pruebas que pasan.

Usa la distribución de pruebas como presupuesto
Una guía práctica recomienda una 70/20/10 división entre pruebas unitarias, de integración y de extremo a extremo, donde las pruebas unitarias ofrecen la retroalimentación más rápida y fallos más estables. La misma guía indica que una suite unitaria completa debería terminar idealmente en menos de 10 segundos, y las comprobaciones pre-commit deben mantenerse menos de 5 segundos, according to this guía de pruebas de OpenReplay.
Lo considero una herramienta de presupuestación, no una religión. Si la mayor parte de tu esfuerzo se va en pruebas de extremo a extremo, tu equipo esperará demasiado tiempo por la retroalimentación. Si todo es solo unitario, te perderás los límites reales del sistema.
Para una app de Capacitor o Electron, un equilibrio saludable suele verse así:
- Tests unitarios para lógica de precios, reglas de permisos, serialización, elegibilidad de actualizaciones, feature flags y transformaciones de estado
- Tests de integración para adaptadores de almacenamiento, wrappers de plugins y contratos IPC
- Pruebas E2E para unos pocos recorridos críticos como inicio de sesión, flujo de compra, sincronización o avisos de actualización
La cobertura es una linterna, no un objetivo
Los informes de cobertura son útiles cuando te ayudan a detectar ramas sin probar en lógica importante. Se vuelven perjudiciales cuando los equipos persiguen porcentajes de cobertura solo por el porcentaje.
Un validador de login con tests de casos límite bien pensados aporta más valor que un archivo con cobertura lleno de aserciones triviales. Esto es especialmente cierto para code con mucha entrada de datos, como formularios, parsers, lógica de fechas y comprobaciones de permisos. Si tu equipo está reforzando la calidad en interfaces con mucha validación, esta guía sobre dominar la validación de formularios frontend es un buen complemento para tu estrategia de unit testing.
Behavior-first tests survive refactors
Una suite fiable debe permitirte refactorizar la lógica interna sin tener que reescribir la mitad de los tests. La forma más fácil de lograrlo es verificar el comportamiento observable en lugar de los detalles de implementación.
Casos de uso que funcionan muy bien:
- Condiciones límite como inputs vacíos, valores tipo null, tipos inválidos y strings demasiado largos
- Resultados del dominio como “devuelve denegado por falta de permiso”
- Transiciones de estado como «marca la actualización como pendiente tras validar los metadatos de descarga»
Casos de uso que suelen degradarse:
- inspeccionar llamadas a helpers internos
- afirmar el orden de métodos privados
- mockear cada capa de la cadena de llamadas
Para equipos de apps que construyen procesos de release disciplinados, el artículo de Capgo sobre control de calidad de la app es útil porque conecta el trabajo de pruebas con el pipeline de release más amplio.
Pruebas para CI, Capacitor y apps de Electron
Una prueba que solo corre en la máquina de un desarrollador no es una red de seguridad. Es un hábito local.
CI convierte el trabajo de unit tests en infraestructura de equipo. Cada push, pull request o rama de release puede ejecutar los mismos comandos con las mismas expectativas. Esa consistencia importa aún más para proyectos de Capacitor y Electron, donde la deriva de entorno causa fallos sutiles.
Haz que CI sea la ruta de ejecución predeterminada
Como mínimo, tu CI debe instalar dependencias y ejecutar la suite de unit tests en cada cambio. Mantén el comando idéntico al de desarrollo local cuando sea posible.
Un workflow básico de GitHub Actions puede ser tan pequeño como este:
name: test
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
Eso es suficiente para detectar imports rotos, aserciones fallidas y suposiciones accidentales de plataforma antes de que lleguen a main.
Para equipos móviles que publican a través de pipelines automatizados, Capgo tiene una guía práctica para configurar CI/CD para apps Capacitor.
Pruebas de interacciones con plugins de Capacitor
La forma incorrecta de hacer pruebas unitarias de Capacitor code es importar plugins nativos directamente en cada servicio. Eso acopla tu suite de pruebas al puente de la plataforma.
El patrón más adecuado es una abstracción fina:
// deviceStorage.js
async function saveFile(filesystem, path, data) {
return filesystem.writeFile({ path, data });
}
module.exports = { saveFile };
// draftService.js
async function persistDraft(storage, draft) {
await storage.save('draft.json', JSON.stringify(draft));
return { saved: true };
}
module.exports = { persistDraft };
// draftService.test.js
const { persistDraft } = require('./draftService');
test('persists a serialized draft', async () => {
const storage = {
save: jest.fn().mockResolvedValue(undefined)
};
const result = await persistDraft(storage, { title: 'Hello' });
expect(result).toEqual({ saved: true });
});
La misma idea aplica al acceso a la cámara, a los prompts biométricos, al registro de push tokens y al estado de la red. Mantén las llamadas a plugins en adaptadores. Prueba la lógica de la app contra interfaces que tú controlas.
Pruebas de main, renderer e IPC de Electron con code
Las aplicaciones de Electron tienen dos puntos clave: proceso principal code y proceso de render code. No los mezcles en las pruebas.
Una configuración fiable suele separar:
- Pruebas unitarias del renderer para view models, estado, formateo y lógica de negocio del lado de la UI
- Pruebas unitarias del proceso principal para menús, operaciones de archivos y decisiones del ciclo de vida de la app
- Pruebas de contrato IPC para la forma del mensaje y las respuestas esperadas
Ejemplo de wrapper IPC:
// ipcGateway.js
function sendSettings(ipcRenderer, payload) {
ipcRenderer.send('settings:update', payload);
}
module.exports = { sendSettings };
// ipcGateway.test.js
const { sendSettings } = require('./ipcGateway');
test('sends settings update over ipc', () => {
const ipcRenderer = { send: jest.fn() };
sendSettings(ipcRenderer, { theme: 'dark' });
expect(ipcRenderer.send).toHaveBeenCalledWith('settings:update', { theme: 'dark' });
});
Si más adelante cambias la implementación interna de un helper a otro, esta prueba sigue siendo válida porque verifica el comportamiento que importa. Ese es el estándar que quieres en desktop y mobile con code.
Preguntas frecuentes sobre pruebas unitarias en JavaScript
¿Cuál es la diferencia entre pruebas unitarias, de integración y E2E?
Una prueba unitaria verifica una pequeña pieza de lógica de forma aislada. Una prueba de integración verifica si varios componentes o servicios funcionan juntos correctamente. Una prueba end-to-end ejecuta un recorrido de usuario a través de la aplicación en ejecución.
Usa pruebas unitarias para tener confianza rápida en las reglas de negocio. Usa pruebas de integración para puntos de unión como almacenamiento, envoltorios de plugins e IPC. Usa pruebas E2E con moderación para los flujos de trabajo que causarían un gran daño si fallaran.
¿Deberíamos apuntar a una cobertura completa?
No. La cobertura total puede llevar a los equipos a crear pruebas de bajo valor.
La cobertura es útil cuando revela code de riesgo que nadie ha probado. No es útil cuando los ingenieros agregan aserciones superficiales solo para satisfacer un dashboard. Si tu suite es frágil, más cobertura no la salvará.
¿Cómo añadimos pruebas a una base de código existente?
Empieza donde ya ocurren los cambios. No congeles al equipo y anuncies una reescritura gigante de la estrategia de pruebas.
Una secuencia práctica es esta:
- Protege primero los code activos añadiendo pruebas a los módulos que modificas al trabajar en nuevas funciones o corregir errores
- Extraer lógica pura de archivos difíciles de probar para que las reglas de negocio se prueben sin ruido del framework o del runtime
- Añadir wrappers de costura alrededor de plugins nativos, clientes de red, llamadas al sistema de archivos y IPC de Electron
- Rechazar patrones frágiles al introducir mocks. La guía de mejores prácticas de testing en JavaScript es especialmente útil aquí, ya que resalta el problema, a menudo pasado por alto, del over-mocking y los tests frágiles que conlleva
El objetivo no es la completitud inmediata, sino mejorar progresivamente en los puntos donde las regresiones más afectan al equipo.
Si tu equipo lanza Capacitor o Electron apps y necesita un proceso de release más limpio para cambios en JavaScript, Capgo es una opción a considerar. Ofrece Live Updates para apps de CapacitorJS y Electron, con controles de rollout y observabilidad, para que los equipos puedan combinar unit testing sólido con una vía más segura para publicar cambios en el web bundle sin esperar la revisión de las tiendas por cada fix.