Cómo probar métodos privados en TypeScript

Cualquier desarrollador de software que escriba pruebas unitarias se enfrenta tarde o temprano a este dilema: escribes una clase compleja con un método auxiliar privado y quieres escribir una prueba unitaria directa para esa lógica en específico.
Cuando intentas llamar a service.handleError() en tu archivo de pruebas, el compilador de TypeScript te bloquea:
Property 'handleError' is private and only accessible within class 'ProductService'.¿Es una mala práctica probar métodos privados? ¿Qué opciones tienes cuando necesitas probar lógica interna crítica?
En este artículo, te voy a mostrar cuatro formas de probar métodos privados en TypeScript, te explicaré la diferencia clave entre private de TypeScript y los campos #private de JavaScript, y revisaremos cuándo conviene refactorizar.
El ejemplo: un servicio con un manejador de errores privado
Imagina este servicio en Angular o Node.js que maneja peticiones HTTP y formatea errores internamente:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ProductService {
#http = inject(HttpClient);
async getProducts(): Promise<string[]> {
try {
const response = await firstValueFrom(
this.#http.get<{ data: string[] }>('/api/products')
);
return response.data;
} catch (error) {
const formatted = this.formatErrorMessage(error as Error);
console.error(formatted);
return [];
}
}
private formatErrorMessage(error: Error): string {
return `[ProductService Error]: ${error.message}`;
}
}El método formatErrorMessage está marcado como private. Veamos las cuatro estrategias para probarlo.
Estrategia 1: Notación de corchetes (el atajo pragmático)
La palabra clave private en TypeScript es solo una comprobación en tiempo de compilación. Una vez que tu código TypeScript se compila a JavaScript, la palabra clave private desaparece por completo.
En tiempo de ejecución, los métodos privados estándar de TypeScript son propiedades normales de JavaScript en el prototipo del objeto. Puedes evitar la verificación del compilador usando el acceso por índice con corchetes:
import { ProductService } from './product.service';
describe('ProductService', () => {
it('should format error messages correctly', () => {
const service = new ProductService();
const testError = new Error('Database connection failed');
// Evitamos la comprobación del compilador de TypeScript usando notación de corchetes
const result = service['formatErrorMessage'](testError);
expect(result).toBe('[ProductService Error]: Database connection failed');
});
});También puedes convertir la instancia a any:
const result = (service as any).formatErrorMessage(testError);Cuándo usar esta opción:
Usa esta opción cuando estés trabajando con código legado y necesites cobertura de pruebas rápida sin refactorizar las clases en producción.
Advertencia: Esto solo funciona con la palabra clave
privatede TypeScript. ¡NO funcionará con los campos#privatede JavaScript!
Diferencia clave: private de TypeScript frente a #private de JavaScript
Muchos desarrolladores confunden la palabra clave private de TypeScript con los campos privados nativos de JavaScript (#methodName).
export class ModernService {
// Privado en tiempo de compilación de TypeScript
private tsPrivate(): string {
return 'Accessible via bracket notation';
}
// Campo privado en tiempo de ejecución de ECMAScript
#jsPrivate(): string {
return 'Enforced by the JS engine';
}
}| Característica | private de TypeScript | #private de ECMAScript |
|---|---|---|
| Aplicación | Solo en tiempo de compilación | Motor nativo de JavaScript en tiempo de ejecución |
Accesible con obj['name'] | Sí (se salta el compilador) | No (lanza error en tiempo de ejecución) |
Accesible con (obj as any) | Sí | No |
| Uso recomendado | Clases generales de la aplicación | Restricciones estrictas de seguridad |
Si tu clase usa #formatErrorMessage, la notación de corchetes va a fallar. Esto nos lleva a nuestras soluciones arquitectónicas más limpias.
Estrategia 2: Extraer a funciones auxiliares puras e independientes (Recomendada)
Cuando un método privado contiene lógica de negocio importante, cálculos o formateo de cadenas, por lo general no debería estar dentro de la clase misma.
Mueve esa lógica a una función pura independiente, ya sea en el mismo archivo o en un archivo de utilidades:
// product.utils.ts
export function formatErrorMessage(error: Error): string {
return `[ProductService Error]: ${error.message}`;
}Luego usa la función dentro de tu servicio:
import { formatErrorMessage } from './product.utils';
export class ProductService {
private format(error: Error) {
return formatErrorMessage(error);
}
}Ahora puedes probar formatErrorMessage directamente sin necesidad de crear instancias de la clase, simular dependencias ni hacer conversiones de tipos:
import { formatErrorMessage } from './product.utils';
describe('formatErrorMessage', () => {
it('formats error with prefix', () => {
const error = new Error('Network timeout');
expect(formatErrorMessage(error)).toBe('[ProductService Error]: Network timeout');
});
});Este enfoque genera código mucho más fácil de probar, reutilizar y mantener.
Estrategia 3: Usar protected y una subclase de prueba
Si el método necesita acceder al estado protegido interno en la instancia de la clase, cambia el modificador de private a protected.
Luego, en tu archivo de pruebas, crea una subclase de prueba que exponga el método protegido de forma pública:
// En tu archivo de pruebas:
class TestableProductService extends ProductService {
public exposeFormatErrorMessage(error: Error): string {
return this.formatErrorMessage(error);
}
}
describe('ProductService with Subclass Sandbox', () => {
it('exposes protected method for verification', () => {
const testService = new TestableProductService();
const result = testService.exposeFormatErrorMessage(new Error('Auth failed'));
expect(result).toBe('[ProductService Error]: Auth failed');
});
});Este patrón mantiene el método oculto para el código consumidor y, al mismo tiempo, le da a tu suite de pruebas un punto de entrada con tipos seguros.
Estrategia 4: Probar a través de métodos públicos (pruebas de comportamiento)
La filosofía más limpia al hacer pruebas es: prueba el comportamiento, no los detalles de implementación.
En lugar de probar formatErrorMessage directamente, prueba el método público getProducts() y verifica su resultado observable cuando ocurre un error:
it('returns empty array and logs error when request fails', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
// Configuramos el mock para que falle
mockHttp.get.mockReturnValue(throwError(() => new Error('Server 500')));
const products = await service.getProducts();
expect(products).toEqual([]);
expect(consoleSpy).toHaveBeenCalledWith('[ProductService Error]: Server 500');
consoleSpy.mockRestore();
});Al probar el contrato público, tus pruebas no se romperán si más adelante decides refactorizar o cambiar de nombre los métodos privados internos.
Resumen comparativo
| Estrategia | Seguridad de tipos | Refactorización requerida | Resistencia a cambios |
|---|---|---|---|
Notación de corchetes (service['method']) | Baja (any) | Ninguna | Baja (falla si el método cambia de nombre) |
| Extraer a función pura | Alta | Menor (mover la función) | Alta (unidad aislada) |
Subclase de prueba con protected | Alta | Baja (cambiar modificador) | Media |
| Prueba de comportamiento público | Alta | Ninguna | Alta (resiste refactorizaciones) |
Mi recomendación:
- Para código existente: Usa la notación de corchetes o prueba el método público.
- Para código nuevo: Extrae la lógica compleja a funciones puras y pruébalas directamente.
Conclusiones y siguientes pasos
Probar métodos privados en TypeScript se reduce a elegir la herramienta adecuada para cada caso:
- Recuerda la diferencia entre compilación y ejecución:
privatede TypeScript desaparece en tiempo de ejecución, pero#privatequeda protegido de forma permanente. - Prioriza el comportamiento público: Prueba lo que el usuario o consumidor ve, no la estructura interna.
- Extrae lógica pura: Las funciones puras sin efectos secundarios en clases siempre son las más fáciles de probar.
Si este artículo te resultó útil, revisa estos artículos relacionados sobre TypeScript y pruebas unitarias:
Mastering the language of the web. From basic types to advanced generics and patterns.
Preguntas Frecuentes
¿Deberías probar métodos privados en TypeScript?
En general, deberías probar los métodos privados de forma indirecta a través de la API pública de tu clase. Si un método privado contiene lógica compleja difícil de probar mediante métodos públicos, suele indicar que esa lógica debería extraerse a un servicio independiente o a una función auxiliar pura.
¿Cómo puedo acceder a un método privado de TypeScript en Jest o Vitest?
Como la palabra clave 'private' de TypeScript solo se valida en tiempo de compilación, puedes evitar la comprobación del compilador en las pruebas usando la notación de corchetes (service['privateMethod']()) o una aserción de tipos ((service as any).privateMethod()).
¿Puede la notación de corchetes acceder a campos privados de JavaScript (#private)?
No. Los campos privados de ECMAScript que usan el prefijo # son aplicados por el motor de JavaScript en tiempo de ejecución. Usar notación de corchetes o conversión de tipos provocará un error de sintaxis en tiempo de ejecución.
¿Cuál es la alternativa más limpia a probar métodos privados?
Extraer la lógica de cálculo o transformación a una función pura independiente y exportada. De esta forma puedes probar la función pura directamente y llamarla desde tu clase.
Artículos Relacionados
Por qué deberías evitar usar 'any' en TypeScript
Por qué usar any en TypeScript genera errores ocultos en tiempo de ejecución, y cómo reemplazarlo con unknown, guardas de tipo personalizadas, tipos de utilidad y el operador satisfies.
Cómo comprobar tipos en TypeScript (Guía completa)
Aprende cómo comprobar y validar tipos en TypeScript en tiempo de compilación y en tiempo de ejecución usando typeof, instanceof, el operador in, uniones discriminadas, type guards personalizados y Zod o Valibot.
Cómo y cuándo usar Mocks vs Spies en pruebas unitarias (Vitest, Jest y Jasmine)
Aprende las diferencias prácticas entre Mocks y Spies en pruebas unitarias. Revisa ejemplos prácticos con Vitest moderno (vi.fn, vi.spyOn), Jest y Jasmine en Angular.
Clases abstractas en TypeScript
En TypeScript, las clases pueden heredar de otras para compartir métodos y propiedades. Además, TypeScript permite usar clases abstractas. Te muestro por qué y cuándo utilizarlas con un ejemplo práctico.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.