Best Practices
Use TestBed for Configuration
Use TestBed to set up and configure a test module. Declare the component you want to test and add required imports, services, or other dependencies.
Tip: Use compileComponents() to ensure that Angular templates and styles are correctly loaded during tests.
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent],
}).compileComponents();
});
Use done, async, and fakeAsync for Asynchronous Code
done– Use when a test completes via a callback (useful for observables).async– Use when you needawait.fakeAsync– Use to simulate time.
it('should fetch data asynchronously', (done) => {
component.data$.subscribe((data) => {
expect(data).toEqual(expectedData);
done();
});
});
it('should fetch data asynchronously', async(() => {
await component.loadData();
expect(component.data).toEqual(expectedData);
}));
it('should fetch data asynchronously', fakeAsync(() => {
component.loadData();
tick(2000); // Simulate 2 seconds
expect(component.data).toEqual(expectedData);
}));
Use DebugElement for DOM Access
DebugElement provides a cross-platform way to manipulate and test DOM elements. It's more reliable than direct access via querySelector.
it('should find a button by CSS and check its text', () => {
const buttonDe = fixture.debugElement.query(By.css('button'));
const buttonEl: HTMLElement = buttonDe.nativeElement;
expect(buttonEl.textContent).toBe('Click Me');
});
Test Input and Output Bindings
it('should pass input value to the component', () => {
component.inputValue = 'Test Input';
fixture.detectChanges();
expect(component.processedValue).toBe('Processed: Test Input');
});
it('should handle the output event from the child component', () => {
const mockChildDebugElement = fixture.debugElement.query(By.directive(MockChildComponent));
const mockChildInstance = mockChildDebugElement.componentInstance;
const handleOutputSpy = jest.spyOn(component, 'handleOutput');
mockChildInstance.myOutput.emit('Test Value');
fixture.detectChanges();
expect(handleOutputSpy).toHaveBeenCalledWith('Test Value');
expect(component.receivedValue).toBe('Test Value');
});
Test Template and DOM Changes
Check how changes in the component (e.g. {{}}, [attr], [class], etc.) reflect in the template.
it('should display updated title', () => {
component.title = 'New Title';
fixture.detectChanges();
const titleElement = fixture.debugElement.query(By.css('h1')).nativeElement;
expect(titleElement.textContent).toBe('New Title');
});
Use detectChanges Correctly
fixture.detectChanges() triggers Angular's change detection and is needed to apply template updates. Use it only when needed.
Tip: Be cautious when using it in beforeEach—the template renders fully only on the first detectChanges.
beforeEach(() => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges(); // Initial render
});
Mock Related Classes
Avoid directly testing services, pipes, or components. Mock them using spyOn() or libraries like mockery.
Test Edge Cases
Write tests for unusual scenarios like empty inputs, null values, or incorrect user actions.
it('should handle null input gracefully', () => {
component.inputValue = null;
fixture.detectChanges();
expect(component.outputValue).toBe('Default Value');
});
Group Tests
Use describe blocks to group related tests and keep your files organized.
describe('Initialization', () => {
it('should create the component', () => {
expect(component).toBeTruthy();
});
});
describe('User Interactions', () => {
it('should toggle state on button click', () => {
component.toggleState();
expect(component.state).toBe(true);
});
it('should toggle state twice on double click', () => {
component.toggleState();
component.toggleState();
expect(component.state).toBe(false);
});
});
Keep Components Testable
Design your components with testability in mind. This typically leads to cleaner, more maintainable code.
Use Faker Factories
When writing unit tests, you often need lots of data. Faker factories help generate valid data efficiently.
Without Faker Factories
Disadvantages:
- Tests break when new required fields are added.
- Refactoring variables doesn’t update them everywhere.
- You define more than needed.
With Faker Factories
Advantages:
- New properties are included automatically.
- Renamed variables update via IDE.
- Faker values fill in missing fields.
- No need to force types.
- Desyncs raise factory errors instead of silent test failures.
const arrangement = FakerFactory.create({title: 'arrangement title'});
const methods = [
FakerMethodFactory.create({code: 'abc'}),
FakerMethodFactory.create({code: 'def'}),
];
Example Factory
import { faker } from '@faker-js/faker';
import { Group } from '../group/index';
export class FakerFactory {
static create(data: Partial<Group>): Group {
return {
id: faker.string.uuid(),
name: faker.word.words({ count: { min: 1, max: 4 } }),
scope: '',
description: faker.lorem.lines(1),
manual: faker.helpers.arrayElement([true, false]),
...data,
} as Group;
}
}
Sources: