⏰ Optimize Angular Component Test Performance

Joost Zöllner
ngxp
Published in
5 min readAug 15, 2018
Waiting for Angular component tests to finish executing…

For a german version of this article visit the Holisticon blog.

Component Tests in Angular are expensive and take a lot of time. That’s why the testing module should include only what is necessary. From our experiences in multiple Angular projects we learned a lot of helpful tips and tricks regarding Angular component tests. This allowed us to save around 90% test execution time. I want to share these tips and tricks with you so you can not only save a lot of time but also a lot of hassle.

Background

In one of our projects the test execution of the Angular tests took around 8–9 minutes. The application had around 100 components and 1000 tests. Because of the long test execution time the automatic deployment via Jenkins and all subsequents processes were delayed. That meant a new build was deployed 15–20 minutes after the commit. This delay was the crucial factor why we decided to analyze our Angular tests. With the new gained knowledge from the analysis we were able to optimize all tests and reduce the test execution time to under 1 minute. As a result we save around 6 hours of time each week as well as a huge amount of developer hassle. ⏳😄

💡 Tips

Before I start with the actual tips, I want to define the general outline of this post. Let’s start by asking why a component should be tested. The goal is to test the functionality of the component. That means we want to create an isolated unit test. But in a lot of cases the testing module imports more than is needed for a unit test. Because of this the unit test becomes an unwanted integration test. That’s problematic as every it-Block inside the test instantiates the testing module, which is expensive. The more modules, components and services are imported in the testing module, the longer the component test takes. To reduce this time I want to give you some tips on how to create isolated Angular component tests and reduce the total test execution time ⏰.

1. Declare components instead of importing modules 📦

In every unit test of a component the testing module is instantiated. Instead of importingthe whole module a component is declared in, you should declare the component inside the testing module itself. Otherwise all other imported modules, declared components and services are also imported into the testing module, even though they are not releveant for the test.

import { AppComponent } from 'app/app.component';
...
TestBed.configureTestingModule({
...
declarations: [
AppComponent
],
...
})

Not every component test can simply be optimized this way. In a lot of cases the component has dependencies on other components, services, pipes or directives. If these are not included in the testing module, the test will fail. But importing them is not the solution as the isolated unit test will then become an integration test. Hence the following tips will give you an overview on how to test components that have additional dependencies.

2. Use the NO_ERRORS_SCHEMA 💣

To create an isolated unit test of a component only the functionality of the component and not its interaction with other child components should be tested. But if the child components are not declared in the testing module and the component references them in the template, the test will fail. This can be mitigated using the NO_ERRORS_SCHEMA. You simply include it inside the schemas array in the testing module. This way all referenced components and directives inside the template won’t give you errors if they are not included in the testing module.

import { NO_ERRORS_SCHEMA } from '@angular/core';
...
TestBed.configureTestingModule({
...
schemas: [
NO_ERRORS_SCHEMA
],
...
})

3. Mock Services 🔨

If a component uses a service the service should be mocked. You don’t want to test the service and usually not every method of a service is used inside a component.

Component

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
constructor(
private authenticationService: AuthenticationService,
) {
this.authenticationService.init()
}
}

Test

const authenticationServiceMock: AuthenticationService = {
init(): void {}
};
TestBed.configureTestingModule({
declarations: [
AppComponent
],
providers: [
{
provide: AuthenticationService,
useValue: authenticationServiceMock
}
]
})

Every @Injectable class can be mocked using this method. That means other class instances can also be mocked inside the testing module. More detailed information on providers can be found on the Angular website: Dependency Injection.

4. Mock Pipes 📐

In most cases mocking pipes is not incredibly beneficial to the test performance. If the pipe does simple text transformation a mocked pipe wouldn’t have a great impact on the test performance. In those cases the original pipe should be added to the testing module instead of a mocked one.

If the pipes are complex or heavy, mocking them can mean performance improvements. Examples of complex and heavy pipes are pipes that have dependencies to other services or pipes that perform asynchronous operations. To mock pipes you create a pipe inside your test that has the same name and include it in the declarations of the testing module

Component

@Component({
selector: 'user-birthday',
template: `
<div>{{ user.birthday | date }}</div>
`,
styles: ['']
})
export class UserBirthdayComponent {
@Input()
user: User;
}

Test

@Pipe({
name: 'date'
})
export class MockDatePipe implements PipeTransform {
transform(value: any, ...args: any[]) {
return value;
}
}
...
TestBed.configureTestingModule({
declarations: [
UserBirthdayComponent,
MockDatePipe
]
})

5. Mock Directives 🔧

Directives are somewhat of a special case. If the directive is used as a attribute inside the template the NO_ERRORS_SCHEMA will catch it. So there is no need to mock it.

<input 
class="form-control"
ngbTooltip="This is a tooltip">
</input>

If the directive is being exported and referenced in the template or the component, it has to be included in the testing module. Otherwise the reference is not defined and you will get errors.

<input 
class="form-control"
ngbTooltip="This is a tooltip"
#tooltip="ngbTooltip"> // Direktive wird exportiert
</input>
<button (click)="tooltip.close()">Close</button>

If the directive is being exported as shown above, the same rule as for the Mock Pipes applies. Mocking simple and lightweight directives will not improve test performance. Only very complex and heavy directives affect the test performance and thus should be mocked. To do this you create a directive with the same selector and exportAs name as it is used in the template. You then add it to the declarations of the testing module.

Component

@Component({
selector: 'user-popover',
template: `
<div #popover="ngbPopover">{{ user.name }}</div>
`,
styles: ['']
})
export class UserPopoverComponent implements OnDestroy {
@ViewChild('popover')
popover: NgbPopover;
ngOnDestroy() {
this.popover.close();
}
}

Test

@Directive({selector: '[ngbPopover]', exportAs: 'ngbPopover'})
export class NgbPopoverMockDirective {
close(): void {}
}
...
TestBed.configureTestingModule({
declarations: [
UserPopoverComponent,
NgbPopoverMockDirective
]
})

Importing whole modules inside component unit tests generally imports a lot more than is needed. This means the unit test will become an integration test that takes a lot more time to execute. That’s why it’s beneficial to isolate the component inside the unit test and mock complex services, pipes or directives.

Not everything that can be mocked, should be mocked

Performance improvements by mocking are often marginal. That’s why you should choose carefully which services, pipes and directives to mock.

I hope I could help you with these tips and wish you: Happy Testing! 🎉🤘

--

--

Joost Zöllner
ngxp
Editor for

Software enthusiast ❤ Web, JavaScript, Angular…