Skip to main content

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 need await.
  • 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
});

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: