In Angular, how do you configure TestBed for a standalone component test, and what belongs in imports versus providers?
answer
- a fresh dynamic module per test
- standalone goes where NgModules go
- doubles belong beside provideRouter
- TestBed.inject, never TestBed.get
basics
~10 sCall TestBed.configureTestingModule with the standalone component and any modules it needs in imports and your service doubles in providers, then create it with TestBed.createComponent; a standalone component never goes in declarations.
solid answer
~30 s`TestBed.configureTestingModule({ imports: [...], providers: [...] })` configures a fresh dynamic testing module for the current test. A standalone component belongs in `imports` — listing it in `declarations` throws — and because it carries its own template dependencies, importing it is optional: `TestBed.createComponent(MyComponent)` compiles it either way. `providers` holds environment-level services and doubles: `provideRouter([])`, `provideHttpClient()`, `{ provide: Api, useValue: fakeApi }`. You read a service back with `TestBed.inject(Api)` (`TestBed.get` was removed in v20). The first `createComponent` or `TestBed.inject` instantiates the module, after which further configuration throws, so all setup comes first.
code
ts · 10 linesTestBed.configureTestingModule({
imports: [OrderSummary],
providers: [
provideRouter([]),
{provide: OrderApi, useValue: {load: () => Promise.resolve({total: 42})}},
],
});
const api = TestBed.inject(OrderApi);
const fixture = TestBed.createComponent(OrderSummary);go deeper
Recall the shape: standalone component in imports, doubles in providers, createComponent to build it, TestBed.inject to read a service. Never declarations for standalone code.
Explain that the testing module is rebuilt around every test, that createComponent or inject instantiates it and freezes configuration, and why importing a standalone component is optional.
Show you keep setup lean: provide*() functions instead of legacy testing modules, only the doubles a spec needs, and a shared helper that still leaves each test in control of its providers.
Frame the suite-wide conventions: which environment providers every spec gets by default, how legacy NgModule specs coexist during migration, and what a reviewer rejects in TestBed setup.
## What TestBed builds `TestBed` (from `@angular/core/testing`) is Angular's test-only way to stand up a slice of an application. Before each test it resets a **dynamic testing module** — an NgModule-shaped container Angular creates for you — and `TestBed.configureTestingModule()` fills it in. When the test first asks for something from it, TestBed compiles what is queued, creates the module's **environment injector**, and from then on hands out services and component fixtures from that injector. The metadata object you pass is a subset of `@NgModule` metadata plus a few test-only options: - `imports` — NgModules and **standalone** components, directives and pipes. - `providers` — providers for the testing module's injector. - `declarations` — only for **non-standalone** (NgModule-era) components, directives and pipes. - `schemas` — such as `NO_ERRORS_SCHEMA`, rarely a good idea. - Test-only options such as `teardown`, `errorOnUnknownElements`, `errorOnUnknownProperties`, `rethrowApplicationErrors` and `deferBlockBehavior`. ## imports, providers and declarations side by side | Array | What goes in it | Standalone-era example | |---|---|---| | `imports` | Standalone components/directives/pipes, NgModules | `imports: [OrderSummary]` | | `providers` | Services, tokens, `provide*()` functions, test doubles | `providers: [provideRouter([]), { provide: Api, useValue: fakeApi }]` | | `declarations` | Non-standalone declarables only | `declarations: [LegacyWidget]` (NgModule code) | Since v19, components, directives and pipes are **standalone by default**, so in new code `declarations` is almost always empty. Putting a standalone component there is an error: TestBed throws with a message saying the class is marked as standalone and cannot be declared, and asks whether you meant to add it to `imports`. Importing the component under test is **optional**. A standalone component carries its own template dependencies, so `TestBed.createComponent(OrderSummary)` works even with `configureTestingModule({})`. The CLI's generated spec still writes `imports: [OrderSummary]` and calls `compileComponents()`; both are harmless. ## A minimal standalone setup ```ts import {TestBed} from '@angular/core/testing'; import {provideRouter} from '@angular/router'; import {OrderSummary} from './order-summary'; import {OrderApi} from './order-api'; describe('OrderSummary', () => { const fakeApi = {load: () => Promise.resolve({total: 42})}; beforeEach(() => { TestBed.configureTestingModule({ imports: [OrderSummary], providers: [provideRouter([]), {provide: OrderApi, useValue: fakeApi}], }); }); it('renders the total', async () => { const fixture = TestBed.createComponent(OrderSummary); await fixture.whenStable(); expect(fixture.nativeElement.textContent).toContain('42'); }); }); ``` Three things make this work: 1. `OrderApi` is resolved from the testing module's injector, where the `useValue` provider shadows the real class (for example one declared `providedIn: 'root'`). 2. `provideRouter([])` gives the component the router services it injects without any real routes. 3. `createComponent` instantiates the module on first use and returns a `ComponentFixture`; driving that fixture is a separate subject. ## Getting services out: TestBed.inject `TestBed.inject(Token)` returns the instance the testing module's injector resolves for a token. It is typed (`TestBed.inject(OrderApi)` is an `OrderApi`) and accepts a fallback value and `inject()`-style options, so `TestBed.inject(Analytics, null)` returns `null` instead of throwing. Its predecessor `TestBed.get` was **removed in v20**. For code that must run in an injection context — a function that calls `inject()` itself — `TestBed.runInInjectionContext(() => ...)` runs it against the same environment injector. `TestBed.inject` reads the **testing module's** injector. It does not see providers a component declares in its own `providers` array; those live in the component's element injector. ## The rule that orders everything The first call that needs the module — `createComponent`, `TestBed.inject` or `runInInjectionContext` — **instantiates** it. After that, `configureTestingModule` and every `override*` method throw, telling you not to use `inject` before configuring. So the shape of every spec is: configure, override, then create and inject. ## NgModule-era specs Most production codebases still contain components declared in NgModules. Their specs look different, and interviewers expect you to read both: - A **non-standalone** component goes in `declarations`, together with any other non-standalone declarables its template uses, or you import the NgModule that declares and exports it. - Anything its template needs from outside — `CommonModule`, `FormsModule`, a shared UI module — goes in `imports`. - Migrating the component to standalone moves those template dependencies onto the component itself, which is why the standalone spec shrinks to a line or two. The provider side is the same in both styles: doubles go in `providers`, and `TestBed.inject` reads them back. ## Mistakes interviewers listen for - Putting a standalone component in `declarations` "because that is where components go". - Recreating the old `HttpClientModule` / `RouterTestingModule` imports instead of `provideHttpClient()` / `provideRouter([])` in `providers`. - Calling `TestBed.get`, which no longer exists. - Configuring once in a `beforeAll`: TestBed resets the testing module around every test, so setup belongs in `beforeEach`. - Expecting `TestBed.inject` to return an instance a component provides for itself.
- Do you have to import a standalone component into the testing module before calling TestBed.createComponent on it?No. A standalone component carries its own template dependencies, so `TestBed.createComponent(MyComponent)` works with an empty `configureTestingModule({})`. Importing it is harmless and the CLI-generated spec does it by convention. Putting it in `declarations` instead is the mistake: TestBed throws and suggests `imports`.
- How do you read an optional service in a spec without the test throwing when nothing provides it?Pass a fallback: `TestBed.inject(Analytics, null)` returns `null` when the token has no provider, or use the options form with `optional: true`. Without the fallback, a missing provider throws a `NullInjectorError` (NG0201).
saying these in an interview costs you the question
- Standalone components are listed in the testing module's declarations array.
- TestBed.get is still the way to read a service in a spec.
- TestBed.inject returns the instance a component provides for itself.
- Configuring TestBed once in beforeAll is enough for the whole file.
- You must import HttpClientModule into the testing module to test HTTP code.