Keep Your Code Testable
Separation Between Component and Services
Components can contain a lot of observables and business logic.
To keep components lightweight, we prefer to move all logic into a component service.
The component service exposes observables that the component can expose 1-to-1 to the template.
All interactions in the template are also passed through the component to the service.
⚠️ Component services should not be provided in root, since they are only used in one place.
Component services must not be reused outside the relevant component.
Component services should be provided in the component itself.
When writing unit tests, make sure to properly mock them:
Example Component Using a Component Service
export class component implements OnInit, OnDestroy {
#titleService = inject(TitleService);
#overviewService = inject(OverviewService);
#methodService = inject(MethodService);
#adminService = inject(AsAdminService);
loadingData$ = this.#overviewService.loadingData$;
overviewData$ = this.#overviewService.overviewData$;
asAdmin$ = this.#adminService.asAdmin$;
methodFetchError$ = this.#methodService.methodFetchError$;
ngOnInit() {
this.#titleService.setKey('overview');
}
toggleAsAdmin = () => this.#asAdminService.toggleAsAdmin();
constructor() {
this.#overviewService.initializeDataFetching();
}
ngOnDestroy() {
this.#overviewService.destroy();
}
}
Templates Should Not Communicate with Themselves
If a template communicates directly with itself, it's no longer possible to intercept with a unit test — in this case, you'll need an integration or end-to-end test.
❌ Bad Example
<div>
<button (click)="myComponent.hide()" />
<myComponent #myComponent />
</div>
✅ Good Example
<div>
<button (click)="hideMyComponent()" />
<myComponent #myComponent />
</div>
Use a data-test Attribute for Every Selector
This prevents tests from breaking due to CSS class or tag name changes.
It also indicates during template refactoring that tests are associated with a specific HTML element.
The
data-testattribute also makes it easier to recover broken tests and can be used by Cypress if needed.
❌ Bad Example
const buttonElement = fixture.debugElement.query(By.css('h1'));
✅ Good Example
const buttonElement = fixture.debugElement.query(By.css('[data-test="title"]'));
Don't Test Existing Interactions
It can be tempting to trigger click events on components, but this ends up testing Angular core features.
Instead, have click events call functions directly, which can also be called by unit tests.
This avoids timing issues often encountered in integration and end-to-end tests.
❌ Bad Example
const buttonElement: HTMLButtonElement = fixture.nativeElement.querySelector('button');
buttonElement.click();
fixture.detectChanges();
expect(component.count).toBe(1);
✅ Good Example
const element: HTMLElement = fixture.debugElement.query(
By.css(`[data-test="my-component"]`)
).nativeElement;
element.nativeElement.dispatchEvent(new Event('clicked'));
expect(component.count).toBe(1);
Use Faker Factories
In unit tests, you often need a lot of data.
Avoid creating objects that loosely resemble the types they’re meant to imitate.
❌ Bad Practice
Disadvantages:
- Old tests break when new required fields are added
- Refactoring variables is not picked up by your IDE
- You often define more than needed for a specific test
✅ Good Practice
Advantages:
- New fields are automatically included
- IDE handles renamed variables
- Unspecified fields are filled with faker values
- No need to force type definitions
- Type mismatches throw factory errors instead of failing tests mysteriously
Example Using a Faker Factory
const list = FakerFactory.create({ title: 'title' });
const methods = [
FakerMethodFactory.create({ code: 'abc' }),
FakerMethodFactory.create({ code: 'def' }),
];
Factory Implementation
import { faker } from '@faker-js/faker';
import { Group } from '../group/index';
export class FakerGroupFactory {
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;
}
}