Components
Testing Components
These tests require creating the component's host element in the browser DOM
, and investigating the component class's interaction with the DOM
as described by its template.
Angular TestBed
facilitates this kind of testing, but in many cases, testing the component class alone, without DOM involvement can validate much of the component's behavior.
Components are more than just a Class
, they interact with DOM and with other components.
Only testing a component's class cannot tell if the component is going to render properly, respond to user input and gestures, or integrate with its parent and child components.
Writing these kind of tests will require TestBed
as well as other testing helpers.
The bare minimum code for DOM
testing is only:
describe("", () => {
const component: NewComponent;
const fixture: ComponentFixture<NewComponent>;
beforeEach(() => {
TestBed.configureTestingModule({ imports: [NewComponent] });
fixture = TestBed.createComponent(NewComponent);
component = fixture.componentInstance;
});
it("should create", () => {
expect(component).toBeDefined();
});
});
createComponent()
createComponent()
This method freezes the current TestBed
definition, closing it to further configuration.
Do not re-configure TestBed
after calling createComponent
.
createComponent
returns a ComponentFixture
.
ComponentFixture
ComponentFixture
Which is a test harness for interacting with the created component and its corresponding element.
DOM Elements Access
nativeElement
nativeElement
ComponentFixture.nativeElement
has an any
type.
Angular cannot know at compile time what kind of HTMLElement
the nativeElement
ir or if it event is an HTMLElement
. (The application might be running on a non-browser plataform, such as the server or a Web Worker)
Knowing that it is an HTMLElement
, you may use querySelector
to dive deeper into the element tree.
describe("", () => {
const component: NewComponent;
const fixture: ComponentFixture<NewComponent>;
beforeEach(() => {
TestBed.configureTestingModule({ imports: [NewComponent] });
fixture = TestBed.createComponent(NewComponent);
component = fixture.componentInstance;
});
it("should create", () => {
expect(component).toBeDefined();
});
it('should have <p> with "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
const p = bannerElement.querySelector("p")!;
expect(p.textContent).toEqual("banner works!");
});
});
DebugElement
DebugElement
Angular fixture provides the component's element directly through the fixture.nativeElement
, which is only a convenience method.
Angular real path to nativeElement
is through fixture.debugElement.nativeElement
.
const bannerElement: HTMLElement = fixture.nativeElement;
const bannerDe: DebugElement = fixture.debugElement;
const bannerEl: HTMLElement = bannerDe.nativeElement;
This is because nativeElement
depend upon the runtime environment, but you could be running these tests on a non-browser plataform, as said before.
So Angular relies on the DebugElement
abstraction to work safely across all supported plataforms.
Instead of creating an HTML element tree, Angular creates a DebugElement
tree that wraps the native elements for the runtime plataform.
it('should find the <p> with fixture.debugElement.nativeElement)', () => {
const bannerDe: DebugElement = fixture.debugElement;
const bannerEl: HTMLElement = bannerDe.nativeElement;
const p = bannerEl.querySelector('p')!;
expect(p.textContent).toEqual('banner works!');
});
Check more of DebugElement
methods and properties here.
By.css()
By.css()
By.css()
, is a static method to select DebugElement
nodes with a standard CSS selector.
It also returns DebugElement
, so you must unwrap it to nativeElement
.
it('should find the <p> with fixture.debugElement.query(By.css)', () => {
const bannerDe: DebugElement = fixture.debugElement;
const paragraphDe = bannerDe.query(By.css('p'));
const p: HTMLElement = paragraphDe.nativeElement;
expect(p.textContent).toEqual('banner works!');
});
detectChanges()
detectChanges()
createComponent
does not bind data, meaning that if you try to test data from signals or other bindings it might not have the expected value.
This happens because createComponent
does not trigger change detection by default.
For dealing with triggering change detection, use fixture.detectChanges()
.
@Component({
selector: 'app-banner',
template: '<h1>{{title()}}</h1>',
styles: ['h1 { color: green; font-size: 350%}'],
})
export class BannerComponent {
title = signal('Test Tour of Heroes');
}
it('should display original title after detectChanges()', () => {
fixture.detectChanges();
expect(h1.textContent).toContain(component.title);
});
it('should display a different test title', () => {
component.title = 'Test Title';
fixture.detectChanges();
expect(h1.textContent).toContain('Test Title');
});
A provider configured in TestBed
to enable automatic change detection.
import {ComponentFixtureAutoDetect} from '@angular/core/testing';
TestBed.configureTestingModule({
providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
});
it('should display original title', () => {
// Hooray! No `fixture.detectChanges()` needed
expect(h1.textContent).toContain(comp.title);
});
it('should still see original title after comp.title change', async () => {
const oldTitle = comp.title;
const newTitle = 'Test Title';
comp.title.set(newTitle);
// Displayed title is old because Angular didn't yet run change detection
expect(h1.textContent).toContain(oldTitle);
await fixture.whenStable();
expect(h1.textContent).toContain(newTitle);
});
it('should display updated title after detectChanges', () => {
comp.title.set('Test Title');
fixture.detectChanges(); // detect changes explicitly
expect(h1.textContent).toContain(comp.title);
});
The second and third test reveal an important limitation.
The Angular testing environment does not run change detection synchronously when updates happen inside the test case that changed the component's title
.
The test must call await fixture.whenStable
to wait for another of change detection.
Examples
Testing Components with async
pipe
async
pipeWhen testing the components, we will mock the ApiService.getFruits()
since the API should not be reached in the test.
@Component({
selector: 'app-fruits-async',
imports: [AsyncPipe],
template: `
@for (fruit of (fruits$ | async); track $index) {
<span data-test-id="fruit-label">{{ fruit }}</span>
}
`
})
export class FruitsAsyncComponent {
private apiService = inject(ApiService);
protected fruits$ = this.apiService.getFruits();
}
const expectedApiFruits = ['grapes', 'strawberries'];
describe('FruitsAsyncComponent', () => {
let component: FruitsAsyncComponent;
let fixture: ComponentFixture<FruitsAsyncComponent>;
let apiServiceMock: jasmine.SpyObj<ApiService>;
beforeEach(async () => {
apiServiceMock = jasmine.createSpyObj('ApiService', ['getFruits']);
// Since 'getFruits' expect an Observable we use 'of()'
apiServiceMock.getFruits.and.returnValue(of(expectedApiFruits));
await TestBed.configureTestingModule({
imports: [FruitsAsyncComponent],
providers: [
{ provide: ApiService, useValue: apiServiceMock }
]
}).compileComponents();
apiServiceMock = TestBed.inject(ApiService) as jasmine.SpyObj<ApiService>;
fixture = TestBed.createComponent(FruitsAsyncComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
it('shoud call fruits api on Init', () => {
fixture.detectChange();
expect(apiServiceMock.getFruits).toHaveBeenCalled();
});
it('should render fruits from api', () => {
fixture.detectChange();
const spanElements = fixture.debugElement.queryAll(By.css('[data-test-id="fruit-label"]'));
spanElements.forEach((spanElement) => {
const hasFruit = expectedApiFruits.includes((spanElement.nativeElement as HTMLElement).textContent);
expect(hasFruit).toBe(true);
});
});
});
Last updated