Skip to main content

Cookbook

danger

This color info panel marks a common pitfalls and very important informations

warning

This color info panel marks a important warning for the user

info

This color info panel marks additional important information

Test Structure

describe('$className$', () => {
beforeAll(() => {
$customElements$
});

beforeEach(async () => {
$subjectImplementations$

await TestBed.configureTestingModule({
imports: [
$components$
$pipes$
$directives$
],
providers: [
$providedInRootServices$
$routerServices$
],
declarations: [
$mockedComponents$
]
});
});
});

Test HTML/DOM

info

All interactions are done using data-test properties on HTML elements. We also use these for CSS selectors when testing. We do not use CSS class selectors for clicks.

Only use the debug element avoid using the native query element. The debug element does have the following advantages:

  1. Abstraction: Debug element is platform independent
  2. Event simulation: Simulates events instead of implementing them.
  3. Cross-platform testing: Debug element supports crossplatform testing

More info: Angular DebugElement

Check Method Calls

it('should spy on method', () => {
const user = {activate: jest.fn()};
const spy = jest.spyOn(user, 'activate');

user.activate();

expect(spy).toHaveBeenCalled();
});

Docs: Jest spyOn

Test with Data Provider

Detailed description and more examples can be found at: https://www.browserstack.com/guide/it-each-jest

function sum(a: number, b: number): number {
return a + b;
}

describe('sum function with it.each', () => {
it.each([
[0, 1, 1],
[5, 3, 8],
[-2, 4, 2],
[10, -5, 5],
[0, 100, 100],
])(
'should return the correct sum for %s + %s = %s',
(a, b, expected) => {
expect(sum(a, b)).toBe(expected);
}
);
});
warning

it.each also supports named properties; however, our Jest setup does not work with them if reference them in the description. This results in tests not being found when you run them.

Limitation: named properties in it.each don’t work with our setup.

Updating Component Inputs

warning

Inputs can only be updated before the first change detection is triggered. After that, it is no longer possible to modify the input without using observables. In some cases you can even ignore change detection when completely relying on change detection.

change detection

describe('OverviewCardComponent', () => {
let component: OverviewCardComponent;
let fixture: ComponentFixture<OverviewCardComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent],
}).compileComponents();

fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});

it('should update input', () => {
fixture.componentRef.setInput('myInput', false);
fixture.detectChanges();
});
});

Globals Testing

info

console tests should be avoided. For example console.error could give linting issues and it will return in the test results as an error.

it('should handle exceptions in the log.', async () => {
const errorLogSpy = jest.spyOn(global.console, 'error').mockImplementation(jest.fn());
console.error('my error');
expect(errorLogSpy).toHaveBeenCalled();
});

Mocking & Overrides

Mocking components

info

You want to avoid having references to components outside your unit in your unit tests. You can do this by mocking components with MockComponents or MockComponent.

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent],
declarations: [
MockComponents(
MyOtherComponent,
MyOtherComponent2,
),
],

You can then use HTML/DOM tests to verify whether the components are actually displayed.

For more info see: How to mock components in Angular tests | ng-mocks

Mocking Services

We prefer to replace services using the MockService method. Angular itself recommends writing mock classes manually in its examples, but we do not follow this approach.

danger

Some services are provided as any. Mocking these service might cause unexpected behaviour. Multiple instance of the service being created, results in undefined observables when you don’t expect it

describe('MyService', () => {
let service: ArrangeOverviewService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
MyService,
MockService(MyOtherService, {users$: of([])})
],
});
service = TestBed.inject(MyService);
});
});

Often, when mocking services, you want to return different data for specific tests. This is possible with a subject:

describe('MyService', () => {
let service: ArrangeOverviewService;
let userSubject: BehaviourSubject<Array<User>>;
beforeEach(() => {
userSubject = new BehaviourSubject([fakerUserFactory.create()])
TestBed.configureTestingModule({
providers: [
MyService,
MockService(MyOtherService, {users$: userSubject.asObservable()})
],
});
service = TestBed.inject(MyService);
});
it('should ....', () => {
const mockService = TestBed.inject(MyOtherService);
userSubject.next([]);
...
})
});

Override Method Calls

it('should empty method', () => {
let userState = 'initial';
const user = {activate: () => userState = 'internal'};

jest.spyOn(user, 'activate').mockImplementation(jest.fn());
user.activate();
expect(userState).toBe('initial');
});

it('should override method', () => {
let userState = 'initial';
const user = {activate: () => userState = 'internal'};

jest.spyOn(user, 'activate').mockImplementation(() => userIsActive = 'mocked');
user.activate();
expect(userState).toBe('mocked');
});

For more info see: The Jest Object · Jest

Override observables

describe('ArrangeOverviewComponent', () => {
let component: ArrangeOverviewComponent;
let fixture: ComponentFixture<ArrangeOverviewComponent>;
let mySubject: BehaviourSubject;

beforeEach(async () => {
mySubject = new BehaviourSubject(true);

await TestBed.configureTestingModule({
providers: [
MyComponent,
MockProvider(MyService, {observable$: mySubject.asObservable()})
],
}).compileComponents();

fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});

it('test observable', async() => {
mySubject.next(false);

expect(await firstValueFrom(component.observable$)).toBe(false);
});
});

Override component services which are provided in a component

describe('my test', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
MyComponent,
],
})
.overrideComponent(MyComponent, {
set: {
providers: [
MockProvider(MyComponentService),
],
},
})
.compileComponents();

fixture = TestBed.createComponent(NewArrangementBannerComponent);
component = fixture.componentInstance;
});

it('should get the mocked service', () => {
const service = fixture.debugElement.injector.get(MyComponentService);

...
})
});

Mocking (standalone) pipesMocking (standalone) pipes

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
MockPipe(MyPipe, (value) => `Mocked: ${value}`),
],
}).compileComponents();
});

it('should ...', () => {
const myPipe = ngMocks.findInstance(MyPipe);

...
})

Mocking Translate Pipe

describe('MyComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
MyComponent,
MockPipe(TranslatePipe, (value) => `Mocked: ${value}`),
],
}).compileComponents();

fixture = TestBed.createComponent(MyComponent);
compiled = fixture.nativeElement;
component = fixture.componentInstance;
});

it.each('testing component', (context: string, amount: number, total: number) => {
const translatePipe = ngMocks.findInstance(TranslatePipe);
expect(translatePipe).toBeDefined();

const translatePipeSpy = jest.spyOn(translatePipe, 'transform');

component.amount = 1;
component.total = 10;
fixture.detectChanges();

expect(translatePipeSpy).toBeCalledWith('arrange.common.search_total', {
filteredAmount: amount,
count: total,
context,
});
expect(compiled.innerHTML).toContain('arrange.common.search_total');
},
);

Mocking Structural Directives

danger

Structural directives that are mocked do not render their body. You need to trigger this manually using ngMocks.render. See the example below.

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
ArrangeOverviewComponent,
MockDirective(HasAdminLicenseDirective),
],
}).compileComponents();

fixture = TestBed.createComponent(ArrangeOverviewComponent);
compiled = fixture.nativeElement;
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should show admin button', () => {
const mockDirective = ngMocks.findInstance(HasAdminLicenseDirective);
ngMocks.render(mockDirective, mockDirective);

expect(
compiled.querySelector(`[data-test="arrange.overview.toggle_admin"]`),
).toBeTruthy();
});

Mocking lit components

beforeAll(() => {
defineCustomHtmlElement('a-tooltip');
defineCustomHtmlElement('a-icon');
});
info

Under the hood, defineCustomHtmlElement creates an inline class (based on a HTMLElement). When you implement this inline, it causes issues with ESLint: max-classes-per-file. defineCustomHtmlElement negates this. max-classes-per-file - ESLint - Pluggable JavaScript Linter

Mocking HTTP Calls

warning

This should only be needed when unit testing services who are directly running HTTP calls.

see: Angular: Mocking Http

Async Testing

With async

it('should await test', async () => {
const var$ = new BehaviorSubject(true);
expect(await firstValueFrom(var$)).toBe(true);
});

With done

info

The use of done is not possible in combination with testing harnesses, as everything works asynchronously with a harness.

it('should done test', (done) => {
const var$ = new BehaviorSubject(true);
var$.subscribe((value) => {
expect(value).toBe(true);
done();
});
});

With fakeAsync and tick

it('should fake async test', fakeAsync(() => {
let value = false;

setTimeout(() => {
value = true;
}, 100);

tick(200);
expect(value).toBe(true);
}));

References: