An Angular suite passes spec by spec but fails in full runs, with services and DOM nodes surviving between tests; how does TestBed's teardown configuration bear on this?
answer
- what happens after each test
- destroyAfterEach and its default
- ngOnDestroy on services
- TestBed resets injectors, not your objects
basics
~20 sWith teardown.destroyAfterEach true, the default, TestBed destroys fixtures and the testing module after each test, running service ngOnDestroy hooks and removing host elements. If it is false, or state lives outside DI, it leaks into the next test.
solid answer
~40 sTestBed resets itself around every test. With `teardown: { destroyAfterEach: true }` — the default — its `afterEach` hook destroys every active fixture, then destroys the testing module, which runs `ngOnDestroy` on services and `DestroyRef` callbacks, and removes the test's host elements from the document. With `destroyAfterEach: false`, the reset waits for the next test's `beforeEach` and the module is never destroyed, so the last host element and any DOM a service appended stay in the document, and services keep their intervals and listeners. `rethrowErrors`, also true by default, makes a failing cleanup fail the test that caused it. If leaks remain with the default, look outside DI: module-scope variables, shared `useValue` objects mutated by one test, `localStorage`, globals.
code
ts · 12 linesdescribe('ShiftBanner', () => {
let fakeClock: {now: () => Date};
beforeEach(() => {
fakeClock = {now: () => new Date(2026, 0, 5, 9, 0)};
TestBed.configureTestingModule({
imports: [ShiftBanner],
teardown: {destroyAfterEach: true, rethrowErrors: true},
});
TestBed.overrideProvider(Clock, {useValue: fakeClock});
});
});go deeper
Know that TestBed destroys fixtures and the testing module after each test by default, which is why each test starts with fresh services.
Explain what destroyAfterEach and rethrowErrors change, where they are configured, and why a service's ngOnDestroy only runs when the module is destroyed.
Diagnose order-dependent failures methodically: find a disabled teardown, then state outside DI such as shared fakes, globals and services without cleanup, and fix the source.
Decide how a large legacy suite moves to default teardown: turn it on, quarantine the specs that relied on leftovers, and treat each fix as a real cleanup bug.
## The symptom Every spec passes on its own; the full run fails. Typical signs: - `document.querySelectorAll` finds elements from earlier tests. - A polling service or global `keydown` listener from a previous test fires in this one. - A counter on a shared fake starts at the value an earlier test left it at. - Errors appear in the output attributed to the wrong test, or to none. Before blaming the runner, check what TestBed destroys between tests and what it cannot. ## What TestBed does between tests `@angular/core/testing` registers a cleanup hook on the runner's global `beforeEach` and `afterEach` (the CLI's Vitest setup registers the same hooks explicitly). Which hook does the work depends on `ModuleTeardownOptions`: ```ts interface ModuleTeardownOptions { destroyAfterEach: boolean; // default true rethrowErrors?: boolean; // default true } ``` When the reset runs, TestBed: 1. **Destroys every active fixture** — component `ngOnDestroy` hooks and `DestroyRef` callbacks run. 2. If `destroyAfterEach` is true, **destroys the testing module** — every service the module's injector created gets `ngOnDestroy`, and injector-scoped `DestroyRef.onDestroy` callbacks run. 3. **Removes the root host elements** it inserted into the document. 4. Clears the configuration, compiled overrides and per-test options, so the next test starts from nothing. ## destroyAfterEach: true versus false | | `destroyAfterEach: true` (default) | `destroyAfterEach: false` | |---|---|---| | When reset runs | `afterEach` of the same test | `beforeEach` of the next test | | Fixtures destroyed | Yes | Yes | | Testing module destroyed | Yes | **No** | | Service `ngOnDestroy` runs | Yes | **No** | | Host elements removed from DOM | Yes | **No** — the last one stays until the next `createComponent` clears old roots | | DOM a service appended (overlay or toast container) | Removed if the service cleans up in `ngOnDestroy` | **Stays** — the service is never destroyed | | Cleanup errors attributed to | The test that caused them | Nothing useful | `false` exists for older suites that were written before module teardown and depend on leftovers. If a suite has it — in `initTestEnvironment(..., { teardown })` or a `configureTestingModule({ teardown })` — that alone explains services, and the DOM they own, surviving. Remove it, then fix the tests that relied on leftovers. ## rethrowErrors With `rethrowErrors` true (its default), cleanup failures fail the test they belong to. Errors thrown while destroying fixtures are each logged, and once all fixtures are destroyed TestBed throws an error reporting how many fixtures failed; an error thrown while destroying the testing module is rethrown as it is. With `rethrowErrors: false`, both are only logged to the console. Leaving it on is what turns a silent leak (an `ngOnDestroy` that throws halfway and never unsubscribes) into a visible failure in the right place. ## Leaks teardown cannot fix TestBed recreates **injectors**, not your JavaScript objects. With the default teardown, remaining cross-test state lives outside dependency injection: - **Shared `useValue` objects.** `const fakeClock = { offset: 0 }` at `describe` scope, provided with `useValue`, is the same object in every test. A test that sets `offset = 60` changes it for all later tests. Create fakes inside `beforeEach` or a setup function. - **Module-scope state** in the code under test: a `let cache` at file level, a static field, a singleton created outside `inject()`. - **Browser globals**: `localStorage`, `sessionStorage`, `document.cookie`, listeners added to `window` by non-Angular code, timers that never belonged to a destroyed service. - **Services that never clean up.** Teardown calls `ngOnDestroy`; it cannot invent one. A service that starts `setInterval` without clearing it on destroy leaks under any setting. ## A diagnosis routine 1. Search the suite and test setup for `teardown` and `destroyAfterEach: false`. 2. Run the failing file alone, then with its neighbour, to find the polluting spec. 3. In the polluting spec, list what it mutates that is not created per test: shared fakes, globals, module-scope variables. 4. Check services it uses for `ngOnDestroy` / `DestroyRef` cleanup of timers, sockets and listeners. 5. Keep `rethrowErrors` on so cleanup failures surface in the test that caused them. The runner's own isolation settings (separate workers or environments per file) can hide these bugs within one file while exposing them in another; they are a runner concern, not a TestBed one.
- Where can teardown be configured, and which setting wins?Suite-wide in the options of `TestBed.initTestEnvironment(...)`, or per test in `configureTestingModule({ teardown })`. The per-test value is checked first, then the environment value, then the default of `destroyAfterEach: true`. Per-test options are cleared at every reset, so they never carry into the next test.
- A service starts a setInterval in its constructor. What does TestBed teardown do about it?Teardown destroys the testing module's injector, which calls the service's `ngOnDestroy` and runs `DestroyRef` callbacks. If the service clears the interval there, it stops; if it has no cleanup, the timer keeps running into later tests under any teardown setting. The fix belongs in the service, and it also fixes the leak in production.
saying these in an interview costs you the question
- destroyAfterEach is false by default, so every suite must opt in to teardown.
- TestBed teardown resets any object you passed with useValue.
- Root-provided services survive from one test to the next.
- Teardown errors are always just logged and never fail a test.
- With destroyAfterEach false, nothing is cleaned up between tests at all.