Cover photo by Marian Kroell on Unsplash. The 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
The first two issues are particularly visible when using Karma, since this test runner executes component tests directly inside a browser.

Did you know? Angular modules and services can respond to the OnDestroy lifecycle moment by implementing an ngOnDestroy method.

This guide covers the following topics:
  • An exploration of the ModuleTeardownOptions#destroyAfterEach option 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, the teardown 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

With destroyAfterEach enabled, teardown runs in response to any of the following events:
  • TestBed.resetTestEnvironment is called
  • TestBed.resetTestingModule is called
  • A test case completes
Next, we'll review complete configuration examples for the Karma and Jest test runners.

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);
Enter fullscreen mode Exit fullscreen mode
test.ts generated by Angular version 12.1 and 13.0
Angular version 12.1 introduces a third parameter for 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);
Enter fullscreen mode Exit fullscreen mode
test.ts generated by Angular version 12.2
For reference, TestBed.configureTestingModule also accepts a teardown option starting in Angular 12.1, demonstrated here:
TestBed.configureTestingModule({
  teardown: { destroyAfterEach: true }, // 👈
  // (...)
});
Enter fullscreen mode Exit fullscreen mode
Test suite setup enabling Angular testing module teardown

Configuring teardown for Jest

If the workspace or project runs unit tests with Jest, the test-setup.ts files typically contain the following:
import 'jest-preset-angular/setup-jest';
Enter fullscreen mode Exit fullscreen mode
test-setup.ts with Angular preset for Jest
To activate teardown in Angular versions 12.1 and 12.2, use this configuration:
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 } }, // 👈
);
Enter fullscreen mode Exit fullscreen mode
test-setup.ts for Jest with Angular testing module teardown
Since the Angular preset for Jest already initializes the testbed, a reset is required before configuring and initializing the testbed environment again. With global teardown configuration covered, we now turn to opting out of this behavior.

Turning off testing module teardown

If enabling destroyAfterEach 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 } }, // 👈
  );
});
Enter fullscreen mode Exit fullscreen mode
To disable teardown for one or more specific test cases, use this snippet:
import { TestBed } from '@angular/core/testing';

beforeEach(() => {
  TestBed.configureTestingModule({
    teardown: { destroyAfterEach: false }, // 👈
    // (...)
  });
});
Enter fullscreen mode Exit fullscreen mode
If a component fixture was already created, you must invoke 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
Enter fullscreen mode Exit fullscreen mode
Before wrapping up, let's examine the performance implications of enabling Angular testing module 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
While this hasn't been measured on a medium or large codebase, the following points are worth considering:
  • 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
The Angular Components team, which uses Karma, has reportedly applied a similar monkey patch since 2017, resulting in faster and more stable test runs.

Conclusion

When Angular testing module teardown is active — by setting ModuleTeardownOptions#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
However, platform-level services never get their 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
In the sections Configuring teardown for Karma and Configuring teardown for Jest, global teardown configurations were provided for both runners. Opting out globally involves calling 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:

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.