Setting the Stage
Unit testing often conjures images of isolating the smallest pieces of an application—individual functions, methods, and classes. This approach works perfectly for a simple function or a standalone class with no dependencies, but the reality of an Angular application is far more complex.
Angular components come bundled with decorators, services, pipes, and templates for rendering data. They often rely on child components to compose their UI, which means there are a significant number of participants involved in even the most basic component test. This article examines the frequent challenges that arise when introducing tests for a single component that has dependencies such as services and child components.
Application Context
Consider an application that uses a PlayerService to retrieve a list of NBA players from an API. The UI is structured around two main components:
Player: A "dumb" component dedicated to displaying the details of a single player.
PlayerListComponent: Fetches the array of players from
PlayerService\and renders them in the view using the Player component.
We will focus our testing efforts on playerlist.component.ts, which brings the complexities of both service and child component dependencies to the table.
Below is the TypeScript for the component:
import { Component, OnInit } from '@angular/core';
import { PlayerService } from 'src/app/services/players.services';
import { APIResponse, Player } from '../models/player';
@Component({
selector: 'app-player-list',
templateUrl: './player-list.component.html',
styleUrls: ['./player-list.component.css'],
})
export class PlayerListComponent implements OnInit {
players: Player[] = [];
constructor(private playerService: PlayerService) { }
ngOnInit(): void {
this.playerService.getPlayers().subscribe((players: APIResponse) => {
this.players = players.data;
});
}
}
Its HTML template incorporates the <app-player> component:
<div *ngFor="let player of players" >
<app-player [player]="player"></app-player>
</div>
Let's dive into writing a test for this component.
Jasmine Insights
Before we proceed, it's worth recalling that Angular’s default testing setup is built on Jasmine and Karma. For those just starting out, here is a concise primer.
describe: Organizes related tests into a block. It takes a descriptive string and a function as its arguments.beforeEach: Executed before each individual test case, it's the go-to place for setup tasks.it: Defines a specific test case. This also requires a string and a function.expect: Used to assert that something matches an expected value or condition in your test.
Find more detailed information on Jasmine and Karma.
The Role of TestBed
Central to Angular's testing is TestBed, a powerful yet straightforward testing environment. It allows components and services to be tested in a controlled, isolated manner, decoupled from the rest of the application. It exposes a number of APIs to configure the test context and interact with the component or service under test. For our purposes, the key methods are configureTestingModule and createComponent.
With configureTestingModule(), we define the test module's configuration. The method takes an object that specifies providers, declarations, imports, and similar options, mirroring the structure used in app.module.ts.
You can explore more details about Testbed
Then, createComponent is used to generate a new component instance, returning a ComponentFixture that facilitates testing the component's logic and its interactions with the DOM.
Let's start by creating the player-list.component.spec.ts file, beginning with a describe block along with a title for the test suite.
describe('PlayerList Component', () => {
})
Next, declare a variable to hold our fixture, which will be typed with ComponentFixture, and another one for the component instance itself, typed as PlayerListComponent.
describe('PlayerList Component', () => {
let fixture: ComponentFixture<PlayerListComponent>
});
Now, it's time to integrate beforeEach with TestBed to establish our testing module. As previously mentioned, configureTestingModule is the tool we need.
We will declare PlayerListComponent and use TestBed.createComponent to instantiate it.
Here is the complete setup snippet:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PlayerListComponent } from './player-list.component';
describe('PlayerList Component', () => {
let fixture: ComponentFixture<PlayerListComponent>
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [PlayerListComponent]
})
fixture = TestBed.createComponent(PlayerListComponent);
})
it('should render the component', () => {
expect(true).toBe(true)
})
})
After saving these changes, go ahead and run the test using npm run test:
✔ Browser application bundle generation complete.
Chrome 108.0.0.0 (Windows 10) PlayerList Component should render the component FAILED
NullInjectorError: R3InjectorError(DynamicTestModule)[PlayerService -> PlayerService]:
NullInjectorError: No provider for PlayerService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'PlayerService', 'PlayerService' ] })
Chrome 108.0.0.0 (Windows 10): Executed 1 of 1 (1 FAILED) (0.046 secs / 0.04 secs)
TOTAL: 1 FAILED, 0 SUCCESS
Running this results in an error. The reason is that our component's constructor is expecting a PlayerService, so we need to supply this dependency without triggering a real API call.
This is where mocking comes into play. Jasmine's createSpyObj function is well-suited for this task. It creates mock objects with spy methods, enabling you to observe and verify function calls without executing the real implementation.
We'll declare a new variable, mockPlayerService, based on jasmine.createSpyObj. It's necessary to explicitly list the methods that we want to mock; for our case, that is getPlayers.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PlayerService } from 'src/app/services/players.services';
import { PlayerListComponent } from './player-list.component';
describe('PlayerList Component', () => {
let fixture: ComponentFixture<PlayerListComponent>
const mockPlayerService = jasmine.createSpyObj<PlayerService>(["getPlayers"]);
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [PlayerListComponent],
providers: [{
provide: PlayerService,
useValue: mockPlayerService
}]
})
fixture = TestBed.createComponent(PlayerListComponent);
})
it('should render the component', () => {
expect(true).toBe(true)
})
})
With this mock in place, the initial error is resolved, and our setup is complete. Now we can move forward and write the actual test cases.
To expand your knowledge, check out this article on Mocks and Spies.
Verifying The Component Under Test
With the testing module configured, the next steps involve validating the component's behavior. Proceed through the following checklist:
[x] Supply a fake API response.
[x] Write a test that fetches players via the service.
[x] Invoke the component's
change-detectioncycle.[x] Confirm the
playersproperty reflects the fake response.
Now, let's construct a test that retrieves players from the mocked service. First, define a variable that mirrors the backend data structure to act as our fake response:
const FAKE_API_RESPONSE: APIResponse = {
data: [{
id: 1,
first_name: 'Lebron'
}]
};
To **test that the component fetches players**, use the it block. Within it, leverage and.returnValue on the spy to return an observable carrying the fake payload. Crucially, invoke change detection through fixture.detectChanges to kick-start the component's lifecycle:
it('should get players from service', () => {
mockPlayerService.getPlayers.and.returnValue(of(FAKE_API_RESPONSE));
fixture.detectChanges();
})
After saving the test, an unexpected failure appears:
PlayerList Component > should get players from the service
Error: NG0304: 'app-player' is not a known element (used in the 'PlayerListComponent' component template):
1. If 'app-player' is an Angular component, then verify that it is a part of an @NgModule where this component is declared.
2. If 'app-player' is a Web Component, then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
Decoding The NG0304: 'app-player' Not A Known Element Error
This failure stems from the component template referencing app-player, which is not declared in our test module. How can we resolve this?
Within the @NgModule decorator, the schemas property controls template validation. Using the NO_ERRORS_SCHEMA constant effectively disables this validation entirely, allowing unknown elements to pass without complaint.
Let's implement that approach:
....CODE COLLAPSED
TestBed.configureTestingModule({
declarations: [PlayerListComponent],
providers: [
{
provide: PlayerService,
useValue: mockPlayerService,
},
],
schemas: [NO_ERRORS_SCHEMA],
});
fixture = TestBed.createComponent(PlayerListComponent);
});
it('should get players from service', () => {
mockPlayerService.getPlayers.and.returnValue(of(FAKE_API_RESPONSE));
fixture.detectChanges();
});
});
Keep in mind this is a temporary fix; we will explore a more robust strategy shortly.
With that change, the suite is green once more. Now, we proceed to verify the component's property values from the fake response.
To inspect the component, the fixture provides several interfaces:
componentInstance: Offers direct access to the instantiated component, enabling checks on its public properties and methods.debugElement: Returns aDebugElementinstance, which acts as a platform-agnostic wrapper around the native DOM element, facilitating interaction with the component's template.debugElement.nativeElement: Provides the raw DOM element associated with the component, allowing for direct inspection of its attributes, styles, and text content.
For this assertion, we use componentInstance to read the players property. Using expect, we compare its length against the single item present in FAKE_API_REPONSE:
it('should get players from service', () => {
mockPlayerService.getPlayers.and.returnValue(of(FAKE_API_RESPONSE));
fixture.detectChanges();
expect(fixture.componentInstance.players.length).toEqual(1)
});
After saving, the test results confirm the behavior:
Handling Child Component Interactions
Recall our earlier template workaround? It was a temporary patch. To truly guarantee the component renders the player list correctly, we must validate the actual template output. So, let's remove the schemas: [NO_ERRORS_SCHEMA] from our test configuration.
Instead of ignoring the issue, we'll mock the <app-player> component, mirroring how we handled the service. Define a separate component within the test file that uses the same app-player selector and includes the properties the test needs. Add a CSS class player to its template for easy selection during queries:
@Component({
selector: 'app-player',
template: `<div class='player'>
<span>{{player.name}}</span>
</div>`
})
class MockPlayer {
@Input() player!: Player;
}
Now, add this MockPlayer component to our testing module's declarations:
TestBed.configureTestingModule({
declarations: [PlayerListComponent, MockPlayer],
providers: [
{
provide: PlayerService,
useValue: mockPlayerService,
},
]
});
What's the rationale here? By registering a mock component with the matching selector in the test module, Angular's dependency injection will provide our lightweight stub whenever the main component's template requests <app-player>. This isolates the test from the real child component while still ensuring the template structure is valid.
Rendering and Querying Child Components in Tests
As with the previous tests, start by assigning the mock data and invoking change detection on the fixture.
The component fixture exposes several methods and properties that let us locate and interact with rendered elements.
queryAll returns an array of DebugElement instances that satisfy a provided predicate.
by.css is a predicate used with queryAll to locate elements matching a specified CSS selector.
by.directive serves as a predicate for queryAll to find elements created with a particular directive or component.
A debugElement is an abstraction that gives access to both the native DOM element and the associated component instance for any node in the test fixture.
Since the mock component template includes a CSS class, the test uses By.css to fetch all elements carrying the div.player class from the rendered DOM.
Declare a variable named totalPlayers and assign it the result of queryAll. This returns an array; use totalPlayer.length to set the expectation for the test.
it('should render the players', () => {
mockPlayerService.getPlayers.and.returnValue(of(FAKE_API_RESPONSE));
fixture.detectChanges();
const totalPlayers = fixture.debugElement.queryAll(By.css('div.player')
expect(totalPlayers).length).toBe(1);
})
Once saved, the tests pass with the mocked service and template in place.
Note: An alternative approach is to rely on By.directive with the component class name.
expect(fixture.debugElement.queryAll(By.directive(MockPlayer)).length).toBe(1);
That wraps up the testing of our component:
[x] Confirm the component retrieves the data.
[x] Confirm the data appears in the DOM.
[x] Learn to mock services and child components.
[x] Learn to query DOM elements with
Byhelpers.
Wrapping Up
We have explored common issues that surface when testing Angular components with multiple dependencies. Building tests with a TestBed, along with mocked services and child components, is essential for verifying the stability and dependability of your application.
Mocking services and child components lets you control their inputs and outputs, simplifying the task of testing the component in isolation.
Applying these practices can strengthen your overall testing approach and lead to more robust, maintainable software.
If you found this helpful, feel free to share it :)
Photo by Louis Reed on Unsplash



