---
name: angular-testing
description: Design and write tests for Angular applications — unit tests with Vitest or Jest, component tests with TestBed and Angular Testing Library, HttpTestingController, testing signals, SignalStore/NgRx, pipes, directives, guards, interceptors, and end-to-end tests with Playwright; plus migrating off Karma/Jasmine. Use this whenever the user asks how to test an Angular component, service, store, form, route guard, or pipe; wants to set up or speed up an Angular test pipeline; asks about mocking HttpClient, testing OnPush components, flaky tests, or "what should I test". Also apply it when generating tests for Angular code the user just wrote, even if they don't say "test".
metadata:
  technology: Angular
  type: testing
---

# Angular Testing

Test behaviour through the public surface: rendered DOM, emitted outputs, HTTP requests made, navigation triggered. Avoid testing private methods or internal signal values directly unless they *are* the public API (a store).

## 1. Runner and setup

- **Vitest** via the Angular CLI unit-test builder (`@angular/build:unit-test`) on current versions; **Jest** (`jest-preset-angular`) on older projects. **Karma is deprecated** — if the user is on it, the first deliverable is a migration plan.
- **Angular Testing Library** (`@testing-library/angular`) for component tests — role-based queries, `userEvent`, less TestBed ceremony.
- `provideZonelessChangeDetection()` in tests if the app is zoneless; otherwise tests behave differently from production.
- Shared `render` helper with default providers (router, HttpClient testing, i18n) in `src/testing/`.
- `provideHttpClient()` + `provideHttpClientTesting()` for HTTP; **MSW** if you want the same mocks across unit, Storybook, and E2E.

## 2. Testing pyramid for Angular

| Layer | Tool | Share | Target |
|---|---|---|---|
| Static | `strict`, `strictTemplates`, `@angular-eslint` (incl. template a11y) | free | Type & template errors |
| Unit | Vitest/Jest, plain functions | ~25% | `model/` logic, pipes, pure helpers, reducers |
| Component/integration | Testing Library + TestBed + HttpTestingController/MSW | ~55% | Page + children rendered together, real store, mocked HTTP |
| Visual/interaction | Storybook + `@storybook/test`, addon-a11y | ~10% | `shared/ui` states |
| E2E | Playwright | ~10% | Critical journeys against a prod build |

## 3. Component tests — patterns

```ts
it('shows an error when the save request fails', async () => {
  const user = userEvent.setup();
  await render(OrderEditPage, {
    providers: [provideHttpClient(), provideHttpClientTesting(), provideRouter([])],
    inputs: { id: '42' },          // signal inputs, incl. route-bound ones
  });
  const http = TestBed.inject(HttpTestingController);
  http.expectOne('/api/orders/42').flush(orderFixture);

  await user.click(screen.getByRole('button', { name: /save/i }));
  http.expectOne({ method: 'PUT', url: '/api/orders/42' }).flush(null, { status: 500, statusText: 'Server Error' });

  expect(await screen.findByRole('alert')).toHaveTextContent(/could not save/i);
  http.verify();
});
```

Rules:
- Query by **role and accessible name**; `data-testid` only with a comment explaining why no role fits.
- **`userEvent`** over `fireEvent`; awaits settle change detection.
- **OnPush/zoneless**: after changing a signal input in a test, use `fixture.componentRef.setInput('x', v)` or Testing Library's `rerender({ inputs })`, then `await fixture.whenStable()`; never call `detectChanges()` blindly in a loop.
- **Outputs**: capture with `on: { save: spy }` in `render` (or `outputRef.subscribe` on the fixture) and assert the emitted payload.
- **Child components**: render them for real. Stub only when the child is heavy (chart library) or has side effects — use a stub with the same selector and `input()`s, or `NO_ERRORS_SCHEMA` sparingly.
- **`@defer` blocks**: use `fixture.getDeferBlocks()` and `deferBlock.render(DeferBlockState.Complete)` to test deferred content; `TestBed.configureTestingModule({ deferBlockBehavior: DeferBlockBehavior.Playthrough })` to trigger naturally.
- **Router**: `provideRouter(routes)` + `RouterTestingHarness` to navigate and assert the activated component; `provideLocationMocks()`.

## 4. Services, stores, and signals

- **Signal services**: `TestBed.runInInjectionContext` or `TestBed.inject(Service)`; call methods; assert `service.items()`; `TestBed.flushEffects()` (or `fixture.detectChanges()`) to run effects.
- **`computed`**: assert values after changing upstream signals — no `await` needed; computeds are synchronous.
- **`toSignal` sources**: provide the Observable via a `Subject` in the mocked dependency, `next()` values, assert the signal.
- **NgRx SignalStore**: instantiate via `TestBed.inject(OrdersStore)` with mocked API providers; call `withMethods` methods; for `rxMethod` use fake HTTP and `await` the settled state. Assert on state signals and computeds, not internals.
- **NgRx Store**: `provideMockStore({ initialState })`, `overrideSelector`, test reducers as pure functions and effects with `provideMockActions` + marbles or `firstValueFrom`.
- **HTTP services**: `HttpTestingController` — `expectOne`, `flush`, assert request body/headers, `verify()` in `afterEach`.

## 5. Pipes, directives, guards, interceptors

- **Pipes**: pure functions — `new MyPipe().transform(input)`; for pipes with DI use `TestBed.runInInjectionContext(() => new Pipe())`.
- **Directives**: render a small host component using the directive; assert DOM/attribute changes and emitted events.
- **Functional guards**: `TestBed.runInInjectionContext(() => canActivateAuth(route, state))`; assert `true`/`UrlTree`.
- **Interceptors**: provide via `provideHttpClient(withInterceptors([authInterceptor]))` and assert on `HttpTestingController` request headers.

## 6. Forms

- Reactive forms: type into inputs with `userEvent`, submit, assert validation messages via `role="alert"` or `aria-describedby`; also assert `form.valid` where the form is the public API of a component.
- Test the *behaviour* of validators (message shown), not `Validators.required` itself.
- Async validators: flush HTTP in `HttpTestingController`, then `await screen.findByText(...)`.

## 7. Storybook

- Stories for every `shared/ui` component state; `argTypes` mapped to `input()`s.
- `play` functions with `@storybook/test` for interaction; run in CI with the test-runner.
- addon-a11y with CI failure on violations for the design system.

## 8. Playwright E2E

- Journeys, not pages: login → create → edit → verify.
- `getByRole` locators; `storageState` for auth; `test.step`.
- Run against `ng build` output served statically (or the SSR server) in CI; never `ng serve`.
- Seed data via API in `beforeEach`; clean up after. Retries only in CI; trace on first retry.
- Tag smoke tests (`@smoke`) to run on every PR; full suite nightly.

## 9. Karma → Vitest/Jest migration plan

1. Add the new runner alongside Karma; convert one feature's specs; get CI green with both.
2. Replace `jasmine.createSpy` → `vi.fn()`/`jest.fn()`, `spyOn` → `vi.spyOn`, `fakeAsync/tick` → keep (works) or fake timers, `done` callbacks → `async/await`.
3. Add Angular Testing Library; rewrite the flakiest specs first with role queries.
4. Remove Karma, `jasmine`, `karma.conf.js`; update `angular.json` `test` target.
5. Add coverage thresholds on changed files only.

## 10. When asked "write tests for this"

1. Enumerate observable behaviours: initial render, each input variation, each user action, each output, loading/error/empty states, a11y roles.
2. Reuse the shared `render` helper and existing fixtures/handlers.
3. One `it` per behaviour, sentence-style names.
4. Run the tests; if you need `detectChanges()` more than once per interaction, the component likely has hidden imperative state — flag it.

## Flaky-test triage

| Symptom | Cause | Fix |
|---|---|---|
| Passes alone, fails in suite | Root-provided service holding state across tests | Reset in `afterEach`, or provide at component level |
| `expectOne` found none / found 2 | Request made in `effect()` or duplicated by `AsyncPipe` resubscribe | `flushEffects`, `shareReplay`, check template |
| Timing failures | `setTimeout` in code, zone vs zoneless mismatch | Fake timers; align test CD mode with app |
| NG0100 in tests only | Writing signals in `ngAfterViewInit` | Fix data flow, don't `setTimeout` |
