Improving Angular tests by enabling Angular testing module teardown
Tags: angular, testing
Published: 2021-10-13 22:22:58

---
title: Improving Angular tests by enabling Angular testing module teardown
series: Configuring the Angular testbed
published: true
description: Introducing the ModuleTeardownOptions#destroyAfterEach option for the Angular testbed.
tags: angular,testing
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x230g63nq1xb0j4laouz.jpg
---

*Cover photo by [Marian Kroell](https://unsplash.com/photos/dfOnZMEzI4Q) on Unsplash.*

The `destroyAfterEach` Angular testing module teardown option addresses several long-standing issues when using the Angular testbed:
- The host element is not removed from the DOM until another component fixture is created
- Component styles are never removed from the DOM
- Application-wide services are never destroyed
- Feature-level services using the any provider scope are never destroyed
- Angular modules are never destroyed
- Components are destroyed 1 time less than the number of tests
- Component-level services are destroyed 1 time less than the number of tests

The two first issues have the biggest impact when using Karma which runs the component tests in a browser.

> **Did you know?** Angular modules and services support hooking into the `OnDestroy` lifecycle moment by implementing an `ngOnDestroy` method.

In this guide, we:
- Explore the `ModuleTeardownOptions#destroyAfterEach` option for the Angular testbed
- List full Angular testing module teardown configurations for Karma and Jest for reference
- Examine how to opt in or opt out of Angular testing module teardown in a test suite or test case
- Talk about the potential performance impact of enabling Angular testing module teardown
- Discuss caveats and remaining issues with the Angular testing module

## Exploring the destroyAfterEach Angular testing module teardown option
Angular version 12.1 adds the `teardown` option object [`ModuleTeardownOptions`](https://angular.io/api/core/testing/ModuleTeardownOptions) which can be passed to `TestBed.configureTestingModule` for a test case or to `TestBed.initTestEnvironment` as a global setting.

We can enable the `destroyAfterEach` option as part of the `teardown` option object. This in turn enables the `rethrowErrors` option which is not covered by this guide.

In Angular versions 12.1 and 12.2, `ModuleTeardownOptions#destroyAfterEach` has a default value of `false`. In Angular version 13.0 and later, its default value is `true`.

When `destroyAfterEach` is enabled, the following happens after each test case or when testing module teardown is otherwise triggered:
- The host element is removed from the DOM
- Component styles are removed from the DOM
- Application-wide services are destroyed
- Feature-level services using the any provider scope are destroyed
- Angular modules are destroyed
- Components are destroyed
- Component-level services are destroyed

> **Angular testing gotcha:** Platform-level services are never destroyed in Angular tests.

### Angular testing teardown triggers
The following events trigger Angular testing teardown when `destroyAfterEach` is enabled:
- `TestBed.resetTestEnvironment` is called
- `TestBed.resetTestingModule` is called
- A test case finishes

Next, let's look at full configuration examples for the Karma and Jest test runners.

## Enabling Angular testing module teardown in Karma
Until Angular version 12.1 (inclusive) and in Angular 13.0 and later versions, a generated main Karma test file (`test.ts`) looks as follows:
```ts
// This file is required by karma.conf.js and loads recursively all the .spec and framework files

import 'zone.js/dist/zone';

import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';

declare const require: any;

// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
```
<figcaption>test.ts generated by Angular version 12.1 and 13.0</figcaption>

Angular version 12.1 adds a 3rd parameter to `TestBed.initTestEnvironment` as seen in the following snippet generated by Angular version 12.2:
```ts
// This file is required by karma.conf.js and loads recursively all the .spec and framework files

import 'zone.js/dist/zone';

import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';

declare const require: any;

// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting(),
  { teardown: { destroyAfterEach: true } }, // 👈
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
```
<figcaption>test.ts generated by Angular version 12.2</figcaption>

For reference, `TestBed.configureTestingModule` also accepts a `teardown` option in Angular 12.1 and later versions as seen in this snippet:
```ts
TestBed.configureTestingModule({
  teardown: { destroyAfterEach: true }, // 👈
  // (...)
});
```
<figcaption>Test suite setup enabling Angular testing module teardown</figcaption>

## Enabling Angular testing module teardown in Jest
If our workspace or project is using Jest for unit tests, `test-setup.ts` files probably look as follows:
```ts
import 'jest-preset-angular/setup-jest';
```
<figcaption>test-setup.ts with Angular preset for Jest</figcaption>

To enable Angular testing module teardown in Angular versions 12.1 and 12.2, use the following code:
```ts
import 'jest-preset-angular/setup-jest';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';

getTestBed().resetTestEnvironment();
getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting(),
  { teardown: { destroyAfterEach: true } }, // 👈
);
```
<figcaption>test-setup.ts for Jest with Angular testing module teardown</figcaption>

The Angular preset for Jest already initializes the Angular testbed environment so we have to reset it before configuring and initializing the Angular testbed environment.

With enabling Angular testing module teardown globally covered, let's move on to opting out of Angular testing module teardown.

## Disabling Angular testing module teardown
If our Angular tests break after enabling Angular testing module teardown, we can opt out globally or locally.

We might want to opt out because various Angular testing libraries might break when `destroyAfterEach` is enabled or they might not accept or specify this option.

Use the following snippet to opt out of Angular testing module teardown in an entire test suite:
```ts
import { TestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';

beforeAll(() => {
  TestBed.resetTestEnvironment();
  TestBed.initTestEnvironment(
    BrowserDynamicTestingModule,
    platformBrowserDynamicTesting(),
    { teardown: { destroyAfterEach: false } }, // 👈
  );
});
```

Use the following snippet to opt out of Angular testing module teardown in one or multiple test cases
```ts
import { TestBed } from '@angular/core/testing';

beforeEach(() => {
  TestBed.configureTestingModule({
    teardown: { destroyAfterEach: false }, // 👈
    // (...)
  });
});
```

If a component fixture has already been created, we must call `TestBed.resetTestingModule` before `TestBed.configureTestingModule`.

Finally, it's possible to opt out of Angular testing module teardown across our entire workspace by applying the optional Angular migration named `migration-v13-testbed-teardown` using the following command:
```powershell
ng update @angular/cli^13 --migrate-only=migration-v13-testbed-teardown
```

Before we conclude, let's discuss the performance impact of Angular testing module teardown.

## Performance impact
The performance impact should always be positive but the level of impact is affected by factors such as:
- Which test runner are we using
- How many testing processes are we running
- How many tests are we running on the same host

I haven't experimented on a medium or large codebase yet but my overall considerations are:
- Removing component style elements and host elements mostly impact Karma because it runs tests in a browser and style evaluation and DOM elements consume resources
- Destroying services and Angular modules prevents duplicate side effects and lets go of resources such as observable subscriptions, HTTP requests, and open web sockets.

The Angular Components team—using Karma—have applied a monkey patch with this functionality in 2017 and they report faster and more reliable tests.

## Conclusion
When Angular testing module teardown is enabled by setting `ModuleTeardownOptions#destroyAfterEach` to `true`, the Angular testbed manages resources between test case runs by triggering the `OnDestroy` lifecycle moment for:
- Application-level services
- Feature-level services
- Angular modules
- Components
- Component-level services

However, the `ngOnDestroy` hooks of platform-level services are never triggered between tests.

Host elements and component styles are removed from the DOM which is especially important when using Karma which runs tests in a browser.

This all happens when `TestBed.resetTestEnvironment` or `TestBed.resetTestingModule` is called or at the latest when a test case finishes.

We discussed how [`ModuleTeardownOptions`](https://angular.io/api/core/testing/ModuleTeardownOptions) were introduced by Angular version 12.1 but that schematics-generated values and default values changed in Angular versions 12.2 and 13.0 as seen in the following table:

| Angular version | Default value of `destroyAfterEach` | Schematics-generated value for `destroyAfterEach` |
| ------ | ------- | ------ |
| <=12.0 | *N/A*   | *N/A*  |
| 12.1   | `false` | *N/A*  |
| 12.2   | `false` | `true` |
| >=13.0 | `true`  | *N/A*  |

In the sections *Enabling Angular testing module teardown in Karma* and *Enabling Angular testing module teardown in Jest*, we referenced full sample global Angular testing module teardown configurations for both the Karma and Jest test runners.

We learnt how we can opt out of Angular testing module teardown on a global level by calling `TestBed.resetTestEnvironment` followed by `TestBed.initTestEnvironment`, specifying the `teardown` option with `destroyAfterEach` set to `false`.

We discussed how to opt out of Angular testing module teardown on one or more test cases by passing a `teardown` option object with `destroyAfterEach` set to `false` to `TestBed.configureTestinModule`, optionally preceded by a call to `TestBed.resetTestingModule`.

Additionally, we learnt how to apply the `migration-v13-testbed-teardown` migration to opt out of Angular testing module teardown across our entire workspace.

Finally, we discussed the potential performance impact of enabling Angular testing module teardown. The potential performance impact is greatest when using Karma because a real DOM is resource-hungry and so is style evaluation when we keep adding stylesheets to a document. Additionally, Karma does not parallelize test runs by default.

Tearing down the Angular testing module is important for test environment correctness but be aware that dependencies provided in the platform scope are never torn down by the Angular testbed implicitly.

## Next steps
Setting the `ModuleTeardownOptions#destroyAfterEach` option to `true` implicitly enables the `ModuleTeardownOptions#rethrowErrors` option which is not covered by this guide.

Enable Angular testing module teardown in your test suites and measure the performance impact using something like [hyperfine](https://github.com/sharkdp/hyperfine).

Let me know of your performance impact and whether any tests failed after enabling this option.

## Resources
Findings in this guide are based on the following Angular pull requests:
- [feat(core): add opt-in test module teardown configuration #42566](https://github.com/angular/angular/pull/42566)
- [Enable test module teardown by default #43353](https://github.com/angular/angular/pull/43353)

I wrote a few hundred tests to compare initialization and teardown behavior when `ModuleTeardownOptions#destroyAfterEach` is enabled and disabled. If you're curious, they're available at [github/LayZeeDK/angular-module-teardown-options](https://github.com/LayZeeDK/angular-module-teardown-options).