| name | angular-testing |
| description | Write, create, generate, debug, and fix Angular unit tests, component tests, service tests, and e2e tests. Vitest is the default test runner for Angular v21+ (also supports Jest, Jasmine, Cypress, Playwright, TestBed). Use when testing components, services, signals, observables, DOM interactions, or mocking dependencies. |
| license | MIT |
| metadata | {"version":"1.0.0","audience":"developers","workflow":"testing"} |
Angular Testing
What I Do
- Write unit tests for Angular components, services, pipes, and directives
- Configure TestBed for component and service testing
- Create mocks, spies, and test doubles for dependencies
- Test signal-based components and computed values
- Handle async testing with fakeAsync, tick, and waitForAsync
- Test HTTP services with HttpTestingController
- Set up e2e tests with Cypress or Playwright
- Debug failing tests and fix test isolation issues
Vitest is the Default (Angular v21+)
As of Angular v21, Vitest is the default test runner for new projects generated by the Angular CLI (ng new). This replaces Karma + Jasmine, which were the default through Angular v20.
What this means in practice:
- New projects ship with
vitest + @vitest/browser (or @vitest/coverage-v8) configured out of the box.
- Spec files use
*.spec.ts and describe/it/expect (Vitest's API is Jest-compatible — minimal migration friction).
- Existing Jest projects work unchanged; Jest is still fully supported.
- Karma-based Jasmine projects should migrate when upgrading to v21+ (Karma is deprecated upstream).
When choosing a runner:
- Vitest (default): Fast, ESM-native, Jest-compatible API, great DX.
- Jest: Still supported; choose if you have a large existing Jest investment or non-Angular Jest tooling.
- Jasmine + Karma: Legacy. New work should not start here. Keep only for in-flight migrations.
Vitest + Angular TestBed still works — TestBed, ComponentFixture, provideHttpClientTesting, etc. are unchanged. The runner changes; the Angular testing APIs do not.
import { defineConfig } from 'vitest/config';
import angular from '@analogjs/vite-plugin-angular';
export default defineConfig({
plugins: [angular()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['src/test-setup.ts'],
include: ['**/*.spec.ts'],
},
});
import { TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from 'vitest';
import { HeroComponent } from './hero.component';
describe('HeroComponent', () => {
it('should display hero name', () => {
expect(fixture.nativeElement.querySelector('.hero-name').textContent)
.toContain('Superman');
});
});
The examples in the rest of this skill use Jest-style APIs (describe, it, expect, vi.fn() / jasmine.SpyObj) which work in Vitest, Jest, and Jasmine with minor adjustments. If you're on Karma + Jasmine, replace vi.fn() with jasmine.createSpy().
When to Use Me
Use this skill when you:
- Write, create, or generate unit tests for Angular components
- Test services with dependencies or HTTP calls
- Debug failing or flaky Angular tests
- Configure TestBed or test module setup
- Mock services, spies, or providers in tests
- Test signal-based reactivity and computed signals
- Handle async operations in tests (Observables, Promises)
- Set up end-to-end testing with Cypress or Playwright
Context7 Integration
For up-to-date Angular testing documentation:
context7_resolve-library-id("Angular", "Angular testing TestBed component service")
context7_query-docs("/angular/angular", "TestBed component testing fakeAsync")
Component Testing
Basic Component Test
import { ComponentFixture, TestBed } from '@angular/core/testing';
describe('HeroComponent', () => {
let component: HeroComponent;
let fixture: ComponentFixture<HeroComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HeroComponent]
});
fixture = TestBed.createComponent(HeroComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should display hero name', () => {
component.hero = { id: 1, name: 'Superman' };
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.hero-name').textContent)
.toContain('Superman');
});
});
Testing Signal Components
it('should update when signal changes', async () => {
component.title.set('Updated Title');
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('h1').textContent)
.toContain('Updated Title');
});
fixture.componentRef.setInput('heroId', 42);
await fixture.whenStable();
OnPush Change Detection
fixture.componentRef.setInput('name', 'New Name');
fixture.detectChanges();
Service Testing
Service with Dependencies
let service: MasterService;
let valueServiceSpy: jasmine.SpyObj<ValueService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('ValueService', ['getValue']);
TestBed.configureTestingModule({
providers: [MasterService, { provide: ValueService, useValue: spy }]
});
service = TestBed.inject(MasterService);
valueServiceSpy = TestBed.inject(ValueService) as jasmine.SpyObj<ValueService>;
});
it('should use injected service', () => {
valueServiceSpy.getValue.and.returnValue('stubbed');
expect(service.getValue()).toBe('stubbed');
});
HTTP Service Testing
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
beforeEach(() => {
TestBed.configureTestingModule({
providers: [HeroService, provideHttpClient(), provideHttpClientTesting()]
});
httpController = TestBed.inject(HttpTestingController);
});
afterEach(() => httpController.verify());
it('should fetch heroes', () => {
service.getHeroes().subscribe(heroes => expect(heroes.length).toBe(2));
const req = httpController.expectOne('api/heroes');
req.flush([{ id: 1, name: 'A' }, { id: 2, name: 'B' }]);
});
Async Testing
fakeAsync and tick
import { fakeAsync, tick, flush } from '@angular/core/testing';
it('should handle async', fakeAsync(() => {
let value = '';
setTimeout(() => value = 'done', 100);
tick(100);
expect(value).toBe('done');
}));
it('should handle delayed observable', fakeAsync(() => {
let result: string;
of('value').pipe(delay(1000)).subscribe(v => result = v);
tick(1000);
expect(result).toBe('value');
}));
Async Helpers
export function asyncData<T>(data: T): Observable<T> {
return defer(() => Promise.resolve(data));
}
serviceSpy.getData.and.returnValue(asyncData({ name: 'Test' }));
DOM Testing
const button = fixture.nativeElement.querySelector('button');
const buttonDe = fixture.debugElement.query(By.css('button'));
button.click();
fixture.detectChanges();
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
input.value = 'new value';
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
component.selected.subscribe(hero => emitted = hero);
button.click();
expect(emitted).toEqual(expectedHero);
E2E Testing
Cypress
describe('Heroes', () => {
beforeEach(() => cy.visit('/heroes'));
it('should display hero list', () => {
cy.get('.hero-item').should('have.length.greaterThan', 0);
});
it('should navigate to detail', () => {
cy.get('.hero-item').first().click();
cy.url().should('match', /\/heroes\/\d+/);
});
});
Playwright
test('should display hero list', async ({ page }) => {
await page.goto('/heroes');
await expect(page.locator('.hero-item')).toHaveCount(10);
});
Router Testing
import { provideRouter } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
describe('UserComponent with routing', () => {
let harness: RouterTestingHarness;
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [
provideRouter([{ path: 'users/:id', component: UserComponent }])
]
});
harness = await RouterTestingHarness.create();
});
it('should load user from route param', async () => {
const component = await harness.navigateByUrl('/users/123', UserComponent);
expect(component.userId()).toBe('123');
});
});
Forms Testing
it('should validate required field', () => {
component.form.controls.name.setValue('');
expect(component.form.controls.name.hasError('required')).toBeTrue();
component.form.controls.name.setValue('John');
expect(component.form.valid).toBeTrue();
});
Common Errors
| Error | Cause | Fix |
|---|
| "No provider for X" | Missing provider | Add to TestBed providers |
| "Cannot read property of undefined" | Missing detectChanges | Call fixture.detectChanges() |
| "timer(s) still in queue" | Unprocessed async | Use flush() or tick() |
| OnPush not updating | CD not triggered | Use setInput() + detectChanges() |
| "Changed after checked" | Value changed during CD | Use autoDetectChanges() |
Best Practices
- Test behavior, not implementation - Focus on user-visible outcomes
- Use data-testid attributes - Avoid fragile CSS selectors
- Mock sparingly - Only mock HTTP and complex external services
- Handle async properly - Always use fakeAsync, waitForAsync, or done()
- Clean up - Verify HTTP requests in afterEach
- Test error paths - Don't just test happy paths
Related Skills
- angular-components - Component architecture and patterns
- angular-state - State management with signals
- typescript-advanced - TypeScript patterns for tests