Cookbook
This color info panel marks a common pitfalls and very important informations
This color info panel marks a important warning for the user
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
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:
- Abstraction: Debug element is platform independent
- Event simulation: Simulates events instead of implementing them.
- 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);
}
);
});
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
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.
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
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
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.
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
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');
});
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
This should only be needed when unit testing services who are directly running HTTP calls.
Async Testing
With async
it('should await test', async () => {
const var$ = new BehaviorSubject(true);
expect(await firstValueFrom(var$)).toBe(true);
});
With done
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: