destroyAfterEach teardown option for the Angular testing module resolves a series of persistent problems when working with the Angular testbed:
- The host element persists in the DOM until the next component fixture is instantiated
- Component styles accumulate in the DOM and are never cleaned up
- Application-wide services do not get destroyed between tests
- Feature-level services using the any provider scope also survive between tests
- Angular modules remain alive after each test
- Components are destroyed one fewer time than the number of executed tests
- Component-level services are destroyed one fewer time than the number of executed tests
This guide covers the following topics:Did you know? Angular modules and services can respond to the
OnDestroylifecycle moment by implementing anngOnDestroymethod.
- An exploration of the
ModuleTeardownOptions#destroyAfterEachoption for the Angular testbed - Complete Angular testing module teardown configurations for Karma and Jest
- How to enable or disable teardown at the test suite or individual test level
- A discussion of the performance implications when enabling teardown
- Caveats and unresolved limitations of the Angular testing module
Understanding the destroyAfterEach teardown option
Starting with Angular version 12.1, theteardown object — defined as ModuleTeardownOptions — can be supplied either to TestBed.configureTestingModule for a single test or to TestBed.initTestEnvironment as a global default.
Setting destroyAfterEach to true within the teardown object activates this behavior. It also implicitly enables the rethrowErrors option, which is not examined in this guide.
For Angular versions 12.1 and 12.2, ModuleTeardownOptions#destroyAfterEach defaults to false. Starting from Angular version 13.0, the default flips to true.
When teardown is active, the following cleanup occurs after every test case or whenever teardown is triggered:
- The host element is detached from the DOM
- Component styles are removed from the DOM
- Application-wide services are destroyed
- Feature-level services with 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 during Angular tests.
What triggers testing teardown
WithdestroyAfterEach enabled, teardown runs in response to any of the following events:
-
TestBed.resetTestEnvironmentis called -
TestBed.resetTestingModuleis called - A test case completes
Configuring teardown for Karma
Up to Angular version 12.1 (inclusive) and again in Angular 13.0 and later, the generated main Karma test file (test.ts) has this structure:// 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);
TestBed.initTestEnvironment, as shown in this snippet from Angular version 12.2:// 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);
TestBed.configureTestingModule also accepts a teardown option starting in Angular 12.1, demonstrated here:TestBed.configureTestingModule({
teardown: { destroyAfterEach: true }, // 👈
// (...)
});
Configuring teardown for Jest
If the workspace or project runs unit tests with Jest, thetest-setup.ts files typically contain the following:import 'jest-preset-angular/setup-jest';
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 } }, // 👈
);
Turning off testing module teardown
If enablingdestroyAfterEach causes tests to fail, it's possible to disable teardown either globally or on a per-test basis.
One reason to opt out is that some Angular testing libraries may not work correctly when teardown is active, or they may not recognize or provide this option.
To disable teardown for an entire test suite, use the following snippet: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 } }, // 👈
);
});
import { TestBed } from '@angular/core/testing';
beforeEach(() => {
TestBed.configureTestingModule({
teardown: { destroyAfterEach: false }, // 👈
// (...)
});
});
TestBed.resetTestingModule before calling TestBed.configureTestingModule.
Finally, teardown can be disabled across an entire workspace by applying the optional Angular migration named migration-v13-testbed-teardown using this command:ng update @angular/cli^13 --migrate-only=migration-v13-testbed-teardown
Performance impact
The performance effect should generally be beneficial, though the magnitude depends on various factors, including:- The test runner in use
- The number of testing processes running concurrently
- The number of tests sharing the same host
- Removing style and host elements matters most in Karma, which executes tests within a browser where DOM nodes and style evaluation consume notable resources
- Destroying services and Angular modules prevents duplicated side effects and releases resources such as observable subscriptions, HTTP requests, and open web sockets
Conclusion
When Angular testing module teardown is active — by settingModuleTeardownOptions#destroyAfterEach to true — the testbed manages resources between test case runs by invoking the OnDestroy lifecycle hook for:
- Application-level services
- Feature-level services
- Angular modules
- Components
- Component-level services
ngOnDestroy hooks triggered between tests.
Host elements and component styles are cleaned up from the DOM, which is particularly relevant for Karma, given its in-browser test execution.
This cleanup runs when TestBed.resetTestEnvironment or TestBed.resetTestingModule is invoked, or at the latest when a test case concludes.
We explored how ModuleTeardownOptions were introduced in Angular 12.1, with changes to both schematics-generated values and defaults in Angular 12.2 and 13.0, summarized 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 |
TestBed.resetTestEnvironment followed by TestBed.initTestEnvironment with the teardown option set to destroyAfterEach: false.
For individual test cases, passing a teardown object with destroyAfterEach: false to TestBed.configureTestingModule works, optionally preceded by a TestBed.resetTestingModule call.
Additionally, the migration-v13-testbed-teardown migration can disable teardown across the entire workspace.
Finally, the performance impact of teardown was discussed. The benefits are most pronounced with Karma since a real DOM and repeated style injection are resource-intensive, and Karma doesn't parallelize test runs by default.
Proper teardown of the Angular testing module is essential for test environment correctness. Just be mindful that platform-scoped dependencies are never implicitly torn down by the Angular testbed.
What to try next
When ModuleTeardownOptions#destroyAfterEach is set to true, the ModuleTeardownOptions#rethrowErrors flag becomes enabled automatically as well — that behavior falls outside the scope of this write-up.
Adopt the teardown option in your own test suites, then quantify any speed differences with a tool such as hyperfine.
Share your observed performance changes and whether any tests started failing once you switched the option on.
References
The conclusions drawn here originate from these Angular pull requests:
- feat(core): add opt-in test module teardown configuration #42566
- Enable test module teardown by default #43353
To examine init and teardown differences with ModuleTeardownOptions#destroyAfterEach toggled both ways, I put together several hundred tests. They are accessible at github/LayZeeDK/angular-module-teardown-options if you'd like to look.
