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.
When agents use itUse 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".
Install
Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.
npx skills add AGCO-Global/org-skills --skill angular-testing -a github-copilot
# or with the org installer (adds .github/skills/angular-testing):
npx -y github:AGCO-Global/org-skills add skill angular-testing
Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.
Installs the frontend-angular-skills plugin, which bundles all Frontend / Angular skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install frontend-angular-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill angular-testing -a claude-code
Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.
Installs into .agents/skills/, which Codex reads.
npx skills add AGCO-Global/org-skills --skill angular-testing -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill angular-testing -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill angular-testing -a gemini-cli
Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.
Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.
Skill contents
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.
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
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.
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
Add the new runner alongside Karma; convert one feature's specs; get CI green with both.
Enumerate observable behaviours: initial render, each input variation, each user action, each output, loading/error/empty states, a11y roles.
Reuse the shared render helper and existing fixtures/handlers.
One it per behaviour, sentence-style names.
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
---
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` |