Testing a component in isolation is straightforward, but the situation quickly becomes complicated when that component relies on child components, directives, or third-party libraries. In this part, we explore a common scenario where our component under test has external dependencies and what happens when we try to run its tests.
As our component tree grows, so does the complexity of the test setup. We might find ourselves considering solutions like NO_ERRORS_SCHEMA, creating stub components, or using the NgMocks library, but each approach carries its own trade-offs that are not always immediately obvious.
Setting Up the Scenario
Imagine an Angular application that still uses the module system, which is a reality for many teams. This application displays a set of discounted products through two related components: ProductComponent and ProductsListComponent.
The ProductComponent is responsible for rendering the list of products, and inside it, the ProductsListComponent leverages a kendo-viewlist for its display. So far, none of these pieces have any test coverage.

Your task is to write tests for the ProductsComponent, but you need to be aware of its two main dependencies:
- It pulls the total count of products on offer from the
products.service. - It renders the
product-list.component, which in turn relies on thekendo-listview.
This seems manageable, so the next step is to execute the test suite.
Executing the Initial Test
The Angular CLI provides us with boilerplate tests for the components it generates. The spec file for the products component, products.component.spec.ts, is auto-generated and looks straightforward, as does the component's TypeScript file.
import { Component, inject } from '@angular/core';
import { ProductsService } from '../services/products/products.service';
@Component({
selector: 'app-products',
templateUrl: './products.component.html',
styleUrl: './products.component.css',
})
export class ProductsComponent {
productService = inject(ProductsService);
total$ = this.productService.totalProductsInOffer;
}
The template for the component is minimal. It retrieves the total product count from the service and dynamically inserts the ProductListComponent into the view.
<div class="bg-white">
<div
class="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:max-w-7xl lg:px-8"
>
@if (total$ | async; as totalProducts) {
<h2 class="text-2xl font-bold tracking-tight text-gray-900">
We have {{ totalProducts }} in offers
</h2>
}
<app-products-list/>
</div>
</div>
Writing a basic test that just verifies the component can be instantiated should, in theory, be a two-minute task. Let's kick off the test runner using the ng test command to see how it goes.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductsComponent } from './products.component';
describe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixture<ProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ProductsComponent],
}).compileComponents();
fixture = TestBed.createComponent(ProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
The result, however, was not a quick success. Instead of a green test, we were immediately met with a flood of errors:
NullInjectorError: R3InjectorError(DynamicTestModule)[ProductsService -> HttpClient -> HttpClient]:

The Source of the NullInjectorError
It's natural to wonder why we are getting a null injector error when the component appears to work in the application. The key detail is that while the module in the app declares ProductService, its dependency, the HttpClient, is not available in the TestBedTestingModule. The solution is to replace the HttpClientModule with its testing counterpart, HttpClientTestingModule, to mock HTTP requests in our test environment.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductsComponent } from './products.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
fdescribe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixture<ProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [ProductsComponent],
}).compileComponents();
fixture = TestBed.createComponent(ProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
After adding the testing module and saving the file, you might expect everything to pass. But running the tests again presents us with a different challenge.
Error: NG0304: 'app-products-list' is not a known element (used in the 'ProductsComponent' component template):
1. If 'app-products-list' is an Angular component, then verify that it is a part of an @NgModule where this component is declared.
2. If 'app-products-list' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
Now we are faced with an error about the app-products-list element within the products-component. The reason is that Angular tries to resolve every component referenced in the test's template. To sidestep this and keep the focus on the component we are actually testing, we could consider applying the NO_ERRORS_SCHEMA to tell Angular to ignore elements that aren't registered. This is a well-known workaround, but is it the best choice for our tests? Let's examine its implications.
Adopting NO_ERRORS_SCHEMA
To wrap up our component testing, one tempting option is to bring in NO_ERRORS_SCHEMA from @angular/core. With this schema in place, the Angular compiler no longer throws errors for unknown elements or attributes in a template; instead, it treats them as ordinary HTML tags.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductsComponent } from './products.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
fdescribe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixture<ProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [ProductsComponent],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
fixture = TestBed.createComponent(ProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Once saved, the tests run clean — everything passes.
The Case Against NO_ERRORS_SCHEMA
That said, leaning on NO_ERRORS_SCHEMA comes with significant downsides. It masks genuine template mistakes and weakens the overall coverage of your tests. The result is a suite prone to false positives, which quietly undermines code quality and accumulates technical debt for later.
Pulling in Child Dependencies
So, if the real trouble is that ProductsListComponent isn't available in the TestBed sandbox, maybe we should simply bring it in. Let's try that.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductsComponent } from './products.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ProductsListComponent } from '../components/products-list/products-list.component';
fdescribe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixture<ProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [ProductsComponent, ProductsListComponent],
}).compileComponents();
And as expected, this leads to needing more and more imports.
Chrome 126.0.0.0 (Mac OS 10.15.7) ProductsComponent should load the product-list FAILED
Error: NG0304: 'kendo-listview' is not a known element (used in the 'ProductsListComponent' component template):
1. If 'kendo-listview' is an Angular component, then verify that it is a part of an @NgModule where this component is declared.
f 'kendo-listview' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
error properties: Object({ code: 304 })
Alright, now we need to pull in the ListViewModule from Kendo as well.
import { ProductsComponent } from './products.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ProductsListComponent } from '../components/products-list/products-list.component';
import { ListViewModule } from '@progress/kendo-angular-listview';
fdescribe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixture<ProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule, ListViewModule],
declarations: [ProductsComponent, ProductsListComponent],
}).compileComponents();
Hold on — are we dragging dependencies into the ProductsComponent test that have no direct relation to it? If ProductListComponent evolves and brings in a form, would that force the ProductComponent test to then load the FormsModule? Our test would fail from a change inside ProductListComponent itself.
Does this feel right, or are we blurring the line between a true unit test and an integration test?
I believe we should keep our testing scope tight. Our focus is on the products themselves: fetching data and showing the offer count. The Kendo rendering is the concern of the product list. A more sensible route is to halt this dependency chain and create a mock for ProductList.
Creating a Component Stub
Our goal is to swap out the actual ProductListComponent. A stub is essentially a simple class that mirrors the original component's selector and presents the same API interface. By stubbing, we gain the ability to query it as a child view, tailor the template for our test's needs, and, crucially, avoid pulling in all those extra dependencies.
Take a new class, decorate it with @Component, and define a simplified template. This is business as usual when writing a component. Here's the initial sketch:
@Component({
selector: 'app-products-list',
template: ` <div>my products</div>`,
})
export class ProductListStub implements ProductsListComponent {
products: Product[] = [];
}
Then, in the declarations, swap out the real ProductsListComponent for our stub. The completed configuration looks like this:
import { Component } from '@angular/core';
import { Product } from '../services/products/products.service';
@Component({
selector: 'app-products-list',
template: ` <div>my products</div>`,
})
export class ProductListMock implements ProductsListComponent {
products: Product[] = [];
}
describe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixture<ProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [ProductsComponent, ProductListStub],
}).compileComponents();
fixture = TestBed.createComponent(ProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
The Cost of Stubbing
Now, stubbing is undeniably neat — it's a fast way to sidestep the complexities of child component dependencies. It also gives us direct control over our test environment. Yet, there's a price attached.
You're on the hook for declaring each component and keeping those stubs current. In a large codebase, maintaining a pile of stubs can become a tangible burden, likely not something your manager wants to budget for. So, let's explore a more elegant alternative: NgMocks.
Introducing NgMocks
ngMocks is a robust library that streamlines Angular testing, making it more readable and cutting down on boilerplate. I enjoy working with it considerably.
It offers a suite of utilities and helpers that extend TestBed and ComponentFixture, simplifying the entire process. With tools like MockBuilder, MockRender, MockComponent, MockDirective, MockPipe, MockService, and AutoMockModule, among others, the amount of setup code you write is drastically reduced.
I won't cover every feature of ngMocks here; I recommend checking out their website or, if you'd like a dedicated article on it, just ask in the comments.
First, install the ng-mocks library:
npm install ng-mocks --save-dev
For those on Angular 15+, remember that
src/test.tsmight be missing. This Stack Overflow answer shows how to restore it.
Next, add the ng-mocks configuration to src/test.ts.
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
import { MockInstance, MockService, ngMocks } from 'ng-mocks';
import { DefaultTitleStrategy, TitleStrategy } from '@angular/router';
import { CommonModule } from '@angular/common';
import { ApplicationModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
{
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
},
);
ngMocks.autoSpy('jasmine');
ngMocks.defaultMock(TitleStrategy, () => MockService(DefaultTitleStrategy));
ngMocks.globalKeep(ApplicationModule, true);
ngMocks.globalKeep(CommonModule, true);
ngMocks.globalKeep(BrowserModule, true);
jasmine.getEnv().addReporter({
specDone: MockInstance.restore,
specStarted: MockInstance.remember,
suiteDone: MockInstance.restore,
suiteStarted: MockInstance.remember,
});
Let's refactor our test, focusing on two key utilities: MockBuilder and MockRender.
MockBuilder takes the hassle out of setting up testing modules by automatically mocking dependencies, which keeps the component under test fully isolated for precise, targeted testing.
MockRender streamlines the creation and rendering of components in your tests, giving you easy access to the component instance and the DOM to check behavior and interactions with confidence.
So, I'm trading TestBed for MockBuilder, passing it the component I want to test and the required module, which in this case is HttpClientTestingModule. Then, I chain a .mock call to specify that ProductList should be mocked.
The .mock method accepts an array if needed, e.g., .mock([ProductsListComponent, AnotherComponent, ...]);
The final test setup looks like this:
import { ProductsComponent } from './products.component';
import { ProductsListComponent } from '../components/products-list/products-list.component';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('ProductsComponent', () => {
beforeEach(() =>
MockBuilder(ProductsComponent)
.keep(HttpClientTestingModule)
.mock(ProductsListComponent),
);
it('should create', () => {
const fixture = MockRender(ProductsComponent);
expect(ngMocks.findInstance(ProductsComponent)).toBeTruthy();
});
});
Save it, and boom! We're all set with only a handful of lines, ready to dive into testing!
Wrapping Up
Testing Angular components takes a turn toward complexity when dependencies enter the picture — be it external libraries or nested child components. What begins as a straightforward test setup quickly escalates into a challenge when a component's template relies on several external pieces.
Turning to NO_ERRORS_SCHEMA can temporarily hide underlying problems. Building stub components offers a cleaner test surface but introduces its own upkeep burden. From my experience, NgMocks stands out as the most practical choice — utilities like MockBuilder and MockRender cut down boilerplate dramatically and let me focus on the actual test logic.
If you're looking for more reference material, check out these resources:

