Pruebas de componentes en Angular: NO_ERRORS_SCHEMA, componentes stub y ng-mocks

Cuando escribo pruebas unitarias para componentes aislados en Angular, el proceso es rápido y predecible. La fricción real comienza cuando el componente depende de componentes hijos, y estos a su vez dependen de librerías de interfaz de usuario (como Kendo UI o Angular Material), módulos de formularios o servicios HTTP.
De pronto, ejecutar una prueba simple como it('should create') falla con errores como NullInjectorError o NG0304: 'app-child' is not a known element.
⚡ ¿Usas Angular Moderno (v17+)? Salta directamente a Angular Moderno: Pruebas en componentes Standalone con
provideHttpClientTesting()para ver cómo configurar TestBed de forma limpia.
En esta guía, evaluaremos las tres estrategias más comunes para resolver dependencias de componentes hijos en pruebas de Angular: NO_ERRORS_SCHEMA, Componentes stub manuales y ng-mocks.
Empecemos revisando nuestro escenario de prueba inicial.
1. El escenario: Componente padre con dependencias hijas profundas 🛍️
Imagina un componente padre que muestra el encabezado de un catálogo de productos y renderiza una lista de productos en un componente hijo:
// products.component.ts (Standalone Moderno)
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { ProductsService } from './products.service';
import { ProductsListComponent } from './products-list.component';
@Component({
selector: 'app-products',
standalone: true,
imports: [AsyncPipe, ProductsListComponent],
template: `
<div class="products-container">
@if (total$ | async; as total) {
<h2>We have {{ total }} special offers today!</h2>
}
<app-products-list />
</div>
`
})
export class ProductsComponent {
private readonly productsService = inject(ProductsService);
readonly total$ = this.productsService.totalOffers$;
}Cuando escribes una prueba unitaria básica con TestBed:
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductsComponent]
}).compileComponents();
});El ejecutor de pruebas falla de inmediato con dos errores muy comunes:
NullInjectorError: No provider for HttpClient!(originado porProductsService).NG0304: 'kendo-listview' is not a known element(originado en lo profundo deProductsListComponent).
Ahora veamos por qué recurrir a NO_ERRORS_SCHEMA es una opción arriesgada.
2. Enfoque 1: NO_ERRORS_SCHEMA (El atajo peligroso) ⚠️
Importar NO_ERRORS_SCHEMA desde @angular/core le dice al compilador de Angular: "Ignora cualquier etiqueta o atributo HTML desconocido y trátalo como un elemento HTML genérico".
import { TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ProductsComponent } from './products.component';
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductsComponent],
schemas: [NO_ERRORS_SCHEMA] // 👈 Silencia los errores de compilación en plantillas
}).compileComponents();
});Por qué NO_ERRORS_SCHEMA es peligroso:
- ❌ Oculta errores reales en la plantilla: Si escribes mal la etiqueta de un componente (
<app-prodcut-list>) o te vinculas a una propiedad que no existe ([prduct]="item"), tus pruebas seguirán pasando en verde. - ❌ Falsa sensación de seguridad: Reduce la cobertura real de las pruebas y permite que errores de plantilla lleguen directamente a producción.
Ahora veamos una alternativa mucho más segura: los componentes stub manuales.
3. Enfoque 2: Componentes stub manuales 🛡️
Un componente stub es una clase simulada y ligera que comparte el mismo selector CSS y las mismas entradas que el componente hijo real, pero con una plantilla vacía:
import { Component, input } from '@angular/core';
import { Product } from './product.model';
@Component({
selector: 'app-products-list',
standalone: true,
template: '<div class="stub-products-list">Mock Products List</div>'
})
export class ProductsListStubComponent {
readonly products = input<Product[]>([]);
}En la configuración de tu prueba, reemplaza el componente real con el stub usando overrideComponent:
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductsComponent]
})
.overrideComponent(ProductsComponent, {
remove: { imports: [ProductsListComponent] },
add: { imports: [ProductsListStubComponent] }
})
.compileComponents();
});Ventajas y desventajas de los stubs manuales:
- ✅ Ventajas: Aislamiento puro, sin necesidad de cargar librerías de interfaz de usuario de terceros y con validación real de plantillas.
- ❌ Desventajas: Alto costo de mantenimiento. Cada vez que cambian las entradas o salidas de los componentes hijos, tienes que actualizar cada stub manualmente.
Ahora revisemos la solución más eficiente: la simulación automatizada con ng-mocks.
4. Enfoque 3: Simulación automatizada con ng-mocks 🚀
ng-mocks es una herramienta de pruebas de código abierto que crea simulaciones seguras a nivel de tipos para componentes, directivas, pipes y servicios en una sola línea de código.
Instálala con:
npm install ng-mocks --save-devCómo usar MockBuilder y MockRender
En lugar de crear stubs a mano o lidiar con llamadas complejas a overrideComponent, puedes apoyarte en MockBuilder:
import { TestBed } from '@angular/core/testing';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { ProductsComponent } from './products.component';
import { ProductsListComponent } from './products-list.component';
describe('ProductsComponent with ng-mocks', () => {
beforeEach(() => {
return MockBuilder(ProductsComponent) // 1. Mantiene real el componente bajo prueba
.mock(ProductsListComponent) // 2. Simula automáticamente el componente hijo
.provide([provideHttpClient(), provideHttpClientTesting()]);
});
it('should create and render child mock cleanly', () => {
const fixture = MockRender(ProductsComponent);
const component = fixture.point.componentInstance;
expect(component).toBeTruthy();
// Verifica que el componente hijo simulado exista en el DOM sin errores
expect(ngMocks.find(fixture, ProductsListComponent)).toBeTruthy();
});
});Ahora veamos cómo Angular moderno (v17+) configura las dependencias de prueba de forma nativa.
5. Angular Moderno: Pruebas en componentes Standalone con provideHttpClientTesting() ⚡
En las versiones modernas de Angular (v17+), ya no importamos HttpClientTestingModule. En su lugar, usamos el proveedor funcional provideHttpClientTesting() junto con provideHttpClient():
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
import { of } from 'rxjs';
import { ProductsComponent } from './products.component';
import { ProductsService } from './products.service';
describe('ProductsComponent (Modern Standalone)', () => {
let serviceMock: jasmine.SpyObj<ProductsService>;
beforeEach(async () => {
serviceMock = jasmine.createSpyObj('ProductsService', [], {
totalOffers$: of(15)
});
await TestBed.configureTestingModule({
imports: [ProductsComponent],
providers: [
provideHttpClient(),
provideHttpClientTesting(),
{ provide: ProductsService, useValue: serviceMock }
]
}).compileComponents();
});
it('should display the total number of offers from service', () => {
const fixture = TestBed.createComponent(ProductsComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h2')?.textContent).toContain('15 special offers');
});
});Matriz comparativa de estrategias 📊
| Estrategia | Complejidad de configuración | Costo de mantenimiento | Seguridad para detectar errores | Mejor caso de uso |
|---|---|---|---|---|
NO_ERRORS_SCHEMA | Baja | Bajo | ❌ Peligroso (Oculta errores) | Prototipos rápidos (evitar en producción) |
| Componentes stub manuales | Media | Alto | ✅ Seguro | Aplicaciones pequeñas con pocos componentes hijos |
ng-mocks (MockBuilder) | Baja | Muy bajo | ✅ Seguro y automatizado | Aplicaciones empresariales, librerías complejas de UI |
Simulación de proveedores (useValue) | Baja | Bajo | ✅ Estricto | Aislamiento de servicios y dependencias |
Resumen 🛠️
Al probar componentes de Angular con dependencias hijas:
- Evita
NO_ERRORS_SCHEMAen suites de pruebas de producción para no ocultar errores reales de plantilla. - Usa
ng-mocks(MockBuilder) para simular componentes hijos sin esfuerzo y sin costos de mantenimiento. - En Angular moderno (v17+), apóyate en
provideHttpClientTesting()e importaciones de componentes Standalone para tener pruebas unitarias limpias y rápidas.
Si quieres conocer más buenas prácticas de pruebas en Angular, revisa mis artículos sobre Cómo compartir datos entre componentes en Angular y Cómo manejar y capturar errores en RxJS y Angular.
These are my experiences learning and facing my daily challenges working with Angular.
Preguntas Frecuentes
¿Por qué NO_ERRORS_SCHEMA es peligroso en las pruebas de componentes de Angular?
NO_ERRORS_SCHEMA le indica al compilador de Angular que ignore los errores de cualquier elemento o atributo HTML no reconocido en las plantillas. Aunque hace que las pruebas pasen de inmediato al ignorar las dependencias hijas, oculta errores tipográficos, enlaces de propiedades rotos e importaciones faltantes, generando falsos positivos.
¿Cuál es la diferencia entre un componente stub y el MockComponent de ng-mocks?
Un componente stub manual requiere crear y mantener una clase @Component simulada que coincida con el selector y las entradas originales. Por su parte, ng-mocks automatiza este proceso simulando componentes hijos, directivas y pipes de forma dinámica en una sola línea usando MockBuilder o MockComponent.
¿Cómo se prueban componentes Standalone con HttpClient en Angular moderno?
En Angular moderno, usa 'provideHttpClient()' y 'provideHttpClientTesting()' dentro de TestBed.configureTestingModule({ providers: [...] }) en lugar de importar el módulo obsoleto HttpClientTestingModule.
¿Cuándo debería usar MockBuilder de ng-mocks en lugar de TestBed?
Usa MockBuilder de ng-mocks cuando la plantilla de tu componente dependa de librerías de interfaz de usuario de terceros complejas (como Kendo UI, Material o gráficos) y quieras probar el componente padre en aislamiento total sin importar árboles enormes de dependencias.
Artículos Relacionados
Cómo compartir datos entre componentes en Angular (Signals y servicios)
Aprende los mejores patrones para compartir datos entre componentes en Angular. Compara los decoradores clásicos Input/Output y BehaviorSubjects con las Signals modernas: input(), output(), model() y servicios basados en Signals.
Cómo usar ng-template, ng-container y ng-content en Angular (Guía completa)
Aprende las diferencias exactas entre ng-template, ng-container y ng-content en Angular, cómo pasar contexto con ngTemplateOutlet, proyección con múltiples slots y cómo evolucionan con el control flow moderno de @if, @for y @defer.
Formularios en Angular: Cómo elegir entre Template-Driven, Reactive y Signal-Driven Forms
Una comparación práctica entre Template-Driven, Reactive y Signal-Driven Forms en Angular. Aprende a crear formularios con validación fuertemente tipada, control flow moderno y estado computado con Signals.
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.