From angular-plugin
Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).
How this skill is triggered — by the user, by Claude, or both
Slash command
/angular-plugin:angular-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
| Layer | Framework |
| Layer | Framework |
|---|---|
| Component, service, pipe, directive unit | Karma + Jasmine (Angular CLI default historically) OR Jest (jest-preset-angular, modern preferred) |
| Component (alt RTL-style) | @testing-library/angular |
| End-to-end | Playwright or Cypress (ng add @cypress/schematic) |
Match what's installed. Modern Angular projects (17+) often switch to Jest for speed; legacy projects stick with Karma.
ng new ships with Karma + Jasmine pre-configured. Tests in *.spec.ts. Run via ng test (watch mode) or ng test --watch=false --browsers=ChromeHeadless (CI).
karma.conf.js and tsconfig.spec.json already in project. Don't modify unless customizing.
pnpm add -D jest jest-preset-angular @types/jest
jest.config.ts:
import type { Config } from 'jest';
const config: Config = {
preset: 'jest-preset-angular',
setupFilesAfterEach: ['<rootDir>/setup-jest.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
};
export default config;
setup-jest.ts:
import 'jest-preset-angular/setup-jest';
Update tsconfig.spec.json:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"]
}
}
In package.json:
"scripts": {
"test": "jest"
}
Remove Karma config / packages from devDependencies (cleanup), or keep both temporarily during migration.
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { UserCardComponent } from './user-card.component';
describe('UserCardComponent', () => {
let fixture: ComponentFixture<UserCardComponent>;
let component: UserCardComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserCardComponent], // standalone — import the component itself
}).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;
});
it('renders user name', () => {
component.user = { id: '1', name: 'Alice', email: '[email protected]' };
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Alice');
});
});
For NgModule projects: TestBed.configureTestingModule({ declarations: [UserCardComponent], imports: [...required modules...] }).
fixture.detectChanges() triggers Angular's change detection — re-render with current state. Required after any input/state change.
beforeEach(async () => {
await TestBed.configureTestingModule({ imports: [UserCardComponent] }).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
});
it('renders user name from signal input', () => {
fixture.componentRef.setInput('user', { id: '1', name: 'Alice', email: '[email protected]' });
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Alice');
});
componentRef.setInput('name', value) is the correct way to set signal-based inputs. Direct component.user = ... won't trigger reactivity.
const mockUsersService = {
loadUsers: jest.fn().mockResolvedValue(undefined),
users: signal([{ id: '1', name: 'Alice' }]),
loading: signal(false),
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UsersListComponent],
providers: [
{ provide: UsersService, useValue: mockUsersService },
],
}).compileComponents();
});
useValue swaps the real service with a mock. useFactory: () => mock for factory-style.
For Karma+Jasmine: replace jest.fn() with jasmine.createSpy('name') or jasmine.createSpyObj('UsersService', ['loadUsers']).
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting(),
],
});
service = TestBed.inject(UsersService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // assert no outstanding requests
});
it('fetches users', () => {
let result: User[] | undefined;
service.loadUsers().subscribe((users) => (result = users));
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush([{ id: '1', name: 'Alice', email: '[email protected]' }]);
expect(result).toEqual([{ id: '1', name: 'Alice', email: '[email protected]' }]);
});
it('handles error', () => {
let error: any;
service.loadUsers().subscribe({
error: (err) => (error = err),
});
const req = httpMock.expectOne('/api/users');
req.error(new ProgressEvent('Network error'), { status: 500 });
expect(error).toBeDefined();
});
});
httpMock.verify() in afterEach fails the test if any HTTP requests were made but not handled — catches accidental real network calls.
@angular/cdk/testing)For Material UI (or custom components with harnesses):
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';
import { MatInputHarness } from '@angular/material/input/testing';
describe('LoginComponent', () => {
let fixture: ComponentFixture<LoginComponent>;
let loader: HarnessLoader;
beforeEach(async () => {
await TestBed.configureTestingModule({ imports: [LoginComponent] }).compileComponents();
fixture = TestBed.createComponent(LoginComponent);
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('logs in on submit', async () => {
const emailInput = await loader.getHarness(MatInputHarness.with({ selector: '[formControlName="email"]' }));
await emailInput.setValue('[email protected]');
const submitBtn = await loader.getHarness(MatButtonHarness.with({ text: /Log in/i }));
await submitBtn.click();
fixture.detectChanges();
expect(/* assert post-submit state */);
});
});
Harnesses provide stable APIs that survive Material upgrades — preferred over By.css('.mat-button') queries.
For custom components, write your own harness extending ComponentHarness. See Material docs.
pnpm add -D @testing-library/angular @testing-library/jest-dom
import { render, screen } from '@testing-library/angular';
import { UserCardComponent } from './user-card.component';
it('renders user name', async () => {
await render(UserCardComponent, {
inputs: { user: { id: '1', name: 'Alice', email: '[email protected]' } },
});
expect(screen.getByText('Alice')).toBeInTheDocument();
});
it('emits delete on click', async () => {
const deleteSpy = jest.fn();
await render(UserCardComponent, {
inputs: { user: { id: '1', name: 'Alice', email: '[email protected]' } },
on: { delete: deleteSpy },
});
const button = screen.getByRole('button', { name: /delete/i });
await userEvent.click(button);
expect(deleteSpy).toHaveBeenCalledWith('1');
});
Same query priority as React/Vue Testing Library: getByRole > getByLabelText > getByText > getByTestId.
For new tests in modern Angular projects, RTL-style is often cleaner than raw TestBed + DOM queries.
pnpm add -D @ngrx/effects @ngrx/store
import { Actions } from '@ngrx/effects';
import { provideMockActions } from '@ngrx/effects/testing';
import { TestBed } from '@angular/core/testing';
import { Observable, of, throwError } from 'rxjs';
import { UsersEffects } from './users.effects';
import { UsersActions } from './users.actions';
describe('UsersEffects', () => {
let actions$: Observable<any>;
let effects: UsersEffects;
let mockHttp: { get: jest.Mock };
beforeEach(() => {
mockHttp = { get: jest.fn() };
TestBed.configureTestingModule({
providers: [
UsersEffects,
provideMockActions(() => actions$),
{ provide: HttpClient, useValue: mockHttp },
],
});
effects = TestBed.inject(UsersEffects);
});
it('loadUsers$ → loadUsersSuccess on HTTP success', (done) => {
const users = [{ id: '1', name: 'Alice' }];
mockHttp.get.mockReturnValue(of(users));
actions$ = of(UsersActions.loadUsers());
effects.loadUsers$.subscribe((action) => {
expect(action).toEqual(UsersActions.loadUsersSuccess({ users }));
done();
});
});
it('loadUsers$ → loadUsersFailure on HTTP error', (done) => {
mockHttp.get.mockReturnValue(throwError(() => new Error('Network down')));
actions$ = of(UsersActions.loadUsers());
effects.loadUsers$.subscribe((action) => {
expect(action).toEqual(UsersActions.loadUsersFailure({ error: 'Network down' }));
done();
});
});
});
provideMockActions(() => actions$) lets you control the input action stream. Effects fire when actions$ emits a matching action.
import { provideMockStore, MockStore } from '@ngrx/store/testing';
beforeEach(() => {
TestBed.configureTestingModule({
imports: [UsersListComponent],
providers: [
provideMockStore({
initialState: { users: { items: [{ id: '1', name: 'Alice' }], loading: false, error: null } },
selectors: [
{ selector: selectUserCount, value: 1 },
],
}),
],
});
store = TestBed.inject(MockStore);
});
it('renders user list from store', () => {
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Alice');
});
it('overrides selector value', () => {
store.overrideSelector(selectUserCount, 5);
store.refreshState();
expect(/* assert based on count = 5 */);
});
ng add @cypress/schematic)// cypress/e2e/login.cy.ts
describe('login', () => {
it('user can log in', () => {
cy.visit('/login');
cy.get('[formControlName=email]').type('[email protected]');
cy.get('[formControlName=password]').type('longenough');
cy.contains('button', /log in/i).click();
cy.url().should('include', '/dashboard');
cy.contains('Welcome').should('be.visible');
});
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
webServer: {
command: 'ng serve',
url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI,
},
use: { baseURL: 'http://localhost:4200', trace: 'on-first-retry' },
});
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.getByLabel(/email/i).fill('[email protected]');
await page.getByLabel(/password/i).fill('longenough');
await page.getByRole('button', { name: /log in/i }).click();
await expect(page).toHaveURL(/\/dashboard/);
});
Target ≥80% on:
Skip / lower bar:
app.module.ts / *.routes.ts (configuration).main.ts (bootstrap).Karma config:
codeCoverage: { include: ['src/**/*.ts'], exclude: ['src/main.ts', 'src/**/*.module.ts', '**/*.spec.ts'] }
Jest config:
coveragePathIgnorePatterns: ['/node_modules/', '\\.module\\.ts$', 'src/main.ts']
The qa-engineer agent has a hard 3-attempt cap on fixing failing tests. After attempt #3, mark xit(...) (Jasmine) or it.skip(...) (Jest) with a comment, report in QA summary.
httpMock.verify() in afterEach — leaves outstanding requests undetected.component.user = X) instead of componentRef.setInput().fixture.detectChanges() after state changes — assertions see stale DOM.getByTestId everywhere instead of accessible queries.provideHttpClientTesting.--watch=true in CI — tests hang waiting for changes.jasmine.createSpy() and jest.fn() in the same project — pick one runner.npx claudepluginhub aratkruglik/claude-sdlc --plugin angular-pluginTests Angular components, services, directives, and pipes using TestBed, ComponentFixture, fakeAsync, and service mocks.
Creates Vitest component tests for Angular views using Angular's own testing idioms (TestBed, ComponentFixture, HttpTestingController).
Write unit and integration tests for Angular v20+ applications with Vitest or Jasmine, covering signals, OnPush, inject(), and HTTP interactions. Activates on test creation and signal-based component testing.