
Angular Testing
- 5.5k installs
- 594 repo stars
- Updated March 23, 2026
- analogjs/angular-skills
Angular v20+ unit and integration testing skill using Vitest or Jasmine with TestBed and modern signal patterns.
About
This skill guides unit and integration testing for Angular v20+ applications using Vitest (recommended) or Jasmine with TestBed and modern patterns. It documents native Vitest support through @angular/build:unit-test in angular.json, install steps for vitest and jsdom, and ng test commands for watch mode and code coverage. Coverage spans signal-based components with computed values and template assertions, OnPush components requiring setInput and detectChanges, injectable services with signal state, HTTP testing via provideHttpClient and HttpTestingController with afterEach verify, and Vitest vi.fn mocks for dependencies including signal-based auth services. Input and output testing uses componentRef.setInput, detectChanges, and output subscriptions. Async patterns cover fakeAsync with tick and flush plus waitForAsync with whenStable. httpResource loading states are tested by flushing mock responses. Agents invoke on test creation, signal component tests, dependency mocking, or test infrastructure setup. Excludes E2E with Cypress or Playwright and non-Angular JavaScript testing.
- Native Vitest setup via @angular/build:unit-test with ng test, watch, and coverage commands
- Signal and computed testing patterns for components, services, and template DOM assertions
- OnPush components tested with setInput and explicit detectChanges for signal inputs
- HTTP and httpResource tests using HttpTestingController flush and verify patterns
- Vitest vi.fn dependency mocks including signal-based auth and user service stubs
Angular Testing by the numbers
- 5,473 all-time installs (skills.sh)
- +102 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #266 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
angular-testing capabilities & compatibility
- Capabilities
- configure vitest angular build · test signal computed components · test onpush setinput · mock services vitest · test http httpresource · async fakeasync waitforasync
- Use cases
- testing · frontend
What angular-testing says it does
Test Angular v20+ applications with Vitest (recommended) or Jasmine, focusing on signal-based components and modern patterns.
Angular v20+ has native Vitest support through the `@angular/build` package.
Don't use for E2E testing with Cypress or Playwright, or for testing non-Angular JavaScript/TypeScript code.
npx skills add https://github.com/analogjs/angular-skills --skill angular-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5.5k |
|---|---|
| repo stars | ★ 594 |
| Security audit | 3 / 3 scanners passed |
| Last updated | March 23, 2026 |
| Repository | analogjs/angular-skills ↗ |
How do I test Angular v20+ components with signals, OnPush, injectable services, and HTTP interactions?
Write unit and integration tests for Angular v20+ with Vitest or Jasmine, TestBed, signals, OnPush, services, and HTTP mocking.
Who is it for?
Angular teams adding or migrating unit tests for signal-based components, services, and HTTP resources.
Skip if: E2E testing with Cypress or Playwright, or testing non-Angular JavaScript and TypeScript code.
When should I use this skill?
User creates Angular tests, tests signal components, mocks dependencies, or sets up Vitest test infrastructure.
What you get
Working Vitest or Jasmine tests with TestBed setup, signal assertions, mocks, and HTTP verification patterns.
- vitest spec files
- harness tests
- router and form test setups
Files
Angular Testing
Test Angular v20+ applications with Vitest (recommended) or Jasmine, focusing on signal-based components and modern patterns.
Vitest Setup (Angular v20+)
Angular v20+ has native Vitest support through the @angular/build package.
npm install -D vitest jsdomConfigure in angular.json:
{
"projects": {
"your-app": {
"architect": {
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json",
"buildTarget": "your-app:build"
}
}
}
}
}
}Run tests:
ng test # Run tests
ng test --watch # Watch mode
ng test --code-coverage # With coverageFor Vitest migration from Jasmine and advanced configuration, see references/vitest-migration.md.
Basic Component Test
import { describe, it, expect, beforeEach } from 'vitest';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Counter } from './counter.component';
describe('Counter', () => {
let component: Counter;
let fixture: ComponentFixture<Counter>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Counter], // Standalone component
}).compileComponents();
fixture = TestBed.createComponent(Counter);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should increment count', () => {
expect(component.count()).toBe(0);
component.increment();
expect(component.count()).toBe(1);
});
it('should display count in template', () => {
component.count.set(5);
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('.count');
expect(element.textContent).toContain('5');
});
});Testing Signals
Direct Signal Testing
import { signal, computed } from '@angular/core';
describe('Signal logic', () => {
it('should update computed when signal changes', () => {
const count = signal(0);
const doubled = computed(() => count() * 2);
expect(doubled()).toBe(0);
count.set(5);
expect(doubled()).toBe(10);
count.update(c => c + 1);
expect(doubled()).toBe(12);
});
});Testing Component Signals
@Component({
selector: 'app-todo-list',
template: `
<ul>
@for (todo of filteredTodos(); track todo.id) {
<li>{{ todo.text }}</li>
}
</ul>
<p>{{ remaining() }} remaining</p>
`,
})
export class TodoList {
todos = signal<Todo[]>([]);
filter = signal<'all' | 'active' | 'done'>('all');
filteredTodos = computed(() => {
const todos = this.todos();
switch (this.filter()) {
case 'active': return todos.filter(t => !t.done);
case 'done': return todos.filter(t => t.done);
default: return todos;
}
});
remaining = computed(() => this.todos().filter(t => !t.done).length);
}
describe('TodoList', () => {
let component: TodoList;
let fixture: ComponentFixture<TodoList>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TodoList],
}).compileComponents();
fixture = TestBed.createComponent(TodoList);
component = fixture.componentInstance;
});
it('should filter active todos', () => {
component.todos.set([
{ id: '1', text: 'Task 1', done: false },
{ id: '2', text: 'Task 2', done: true },
{ id: '3', text: 'Task 3', done: false },
]);
component.filter.set('active');
expect(component.filteredTodos().length).toBe(2);
expect(component.remaining()).toBe(2);
});
});Testing OnPush Components
OnPush components require explicit change detection:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<span>{{ data().name }}</span>`,
})
export class OnPushCmpt {
data = input.required<{ name: string }>();
}
describe('OnPushCmpt', () => {
it('should update when input signal changes', () => {
const fixture = TestBed.createComponent(OnPushCmpt);
// Set input using setInput (for signal inputs)
fixture.componentRef.setInput('data', { name: 'Initial' });
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Initial');
// Update input
fixture.componentRef.setInput('data', { name: 'Updated' });
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Updated');
});
});Testing Services
Basic Service Test
@Injectable({ providedIn: 'root' })
export class CounterService {
private _count = signal(0);
readonly count = this._count.asReadonly();
increment() { this._count.update(c => c + 1); }
reset() { this._count.set(0); }
}
describe('CounterService', () => {
let service: CounterService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CounterService);
});
it('should increment count', () => {
expect(service.count()).toBe(0);
service.increment();
expect(service.count()).toBe(1);
});
});Service with HTTP
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting(),
],
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Verify no outstanding requests
});
it('should fetch user by id', () => {
const mockUser = { id: '1', name: 'Test User' };
service.getUser('1').subscribe(user => {
expect(user).toEqual(mockUser);
});
const req = httpMock.expectOne('/api/users/1');
expect(req.request.method).toBe('GET');
req.flush(mockUser);
});
});Mocking Dependencies
Using Vitest Mocks
import { describe, it, expect, vi, beforeEach } from 'vitest';
describe('UserProfile', () => {
const mockUserService = {
getUser: vi.fn(),
updateUser: vi.fn(),
user: signal<User | null>(null),
};
beforeEach(async () => {
vi.clearAllMocks();
mockUserService.getUser.mockReturnValue(of({ id: '1', name: 'Test' }));
await TestBed.configureTestingModule({
imports: [UserProfile],
providers: [
{ provide: UserService, useValue: mockUserService },
],
}).compileComponents();
});
it('should call getUser on init', () => {
const fixture = TestBed.createComponent(UserProfile);
fixture.detectChanges();
expect(mockUserService.getUser).toHaveBeenCalledWith('1');
});
});Mock Signal-Based Service
const mockAuth = {
user: signal<User | null>(null),
isAuthenticated: computed(() => mockAuth.user() !== null),
login: vi.fn(),
logout: vi.fn(),
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProtectedPage],
providers: [
{ provide: AuthService, useValue: mockAuth },
],
}).compileComponents();
});
it('should show content when authenticated', () => {
mockAuth.user.set({ id: '1', name: 'Test User' });
const fixture = TestBed.createComponent(ProtectedPage);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.protected-content')).toBeTruthy();
});Testing Inputs and Outputs
@Component({
selector: 'app-item',
template: `<div (click)="select()">{{ item().name }}</div>`,
})
export class ItemCmpt {
item = input.required<Item>();
selected = output<Item>();
select() {
this.selected.emit(this.item());
}
}
describe('ItemCmpt', () => {
it('should emit selected event on click', () => {
const fixture = TestBed.createComponent(ItemCmpt);
const item: Item = { id: '1', name: 'Test Item' };
fixture.componentRef.setInput('item', item);
fixture.detectChanges();
let emittedItem: Item | undefined;
fixture.componentInstance.selected.subscribe(i => emittedItem = i);
fixture.nativeElement.querySelector('div').click();
expect(emittedItem).toEqual(item);
});
});Testing Async Operations
Using fakeAsync
import { fakeAsync, tick, flush } from '@angular/core/testing';
it('should debounce search', fakeAsync(() => {
const fixture = TestBed.createComponent(SearchCmpt);
fixture.detectChanges();
fixture.componentInstance.query.set('test');
tick(300); // Advance time for debounce
fixture.detectChanges();
expect(fixture.componentInstance.results().length).toBeGreaterThan(0);
flush(); // Flush remaining timers
}));Using waitForAsync
import { waitForAsync } from '@angular/core/testing';
it('should load data', waitForAsync(() => {
const fixture = TestBed.createComponent(DataCmpt);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.componentInstance.data()).toBeDefined();
});
}));Testing HTTP Resources
@Component({
template: `
@if (userResource.isLoading()) {
<p>Loading...</p>
} @else if (userResource.hasValue()) {
<p>{{ userResource.value().name }}</p>
}
`,
})
export class UserCmpt {
userId = signal('1');
userResource = httpResource<User>(() => `/api/users/${this.userId()}`);
}
describe('UserCmpt', () => {
let httpMock: HttpTestingController;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserCmpt],
providers: [
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
httpMock = TestBed.inject(HttpTestingController);
});
it('should display user name after loading', () => {
const fixture = TestBed.createComponent(UserCmpt);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Loading');
const req = httpMock.expectOne('/api/users/1');
req.flush({ id: '1', name: 'John Doe' });
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('John Doe');
});
});For advanced testing patterns including component harnesses, router testing, form testing, and directive testing, see references/testing-patterns.md.
For Vitest migration from Jasmine, see references/vitest-migration.md.
Angular Testing Patterns
Table of Contents
- Vitest Advanced Patterns
- Component Harnesses
- Testing Router
- Testing Forms
- Testing Directives
- Testing Pipes
- E2E Testing Setup
Vitest Advanced Patterns
Snapshot Testing
import { describe, it, expect } from 'vitest';
describe('UserCard', () => {
it('should match snapshot', () => {
const fixture = TestBed.createComponent(UserCard);
fixture.componentRef.setInput('user', { id: '1', name: 'John', email: 'john@example.com' });
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toMatchSnapshot();
});
});Parameterized Tests
import { describe, it, expect } from 'vitest';
describe('Validator', () => {
it.each([
{ input: '', expected: false },
{ input: 'test', expected: false },
{ input: 'test@example.com', expected: true },
{ input: 'invalid@', expected: false },
])('should validate email "$input" as $expected', ({ input, expected }) => {
expect(isValidEmail(input)).toBe(expected);
});
});Testing with Fake Timers
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
describe('Debounced Search', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should debounce search input', async () => {
const fixture = TestBed.createComponent(Search);
fixture.detectChanges();
fixture.componentInstance.query.set('test');
// Search not called yet
expect(fixture.componentInstance.results()).toEqual([]);
// Advance timers
vi.advanceTimersByTime(300);
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.componentInstance.results().length).toBeGreaterThan(0);
});
});Module Mocking
import { describe, it, expect, vi } from 'vitest';
// Mock entire module
vi.mock('./analytics.service', () => ({
Analytics: class {
track = vi.fn();
identify = vi.fn();
},
}));
describe('with mocked analytics', () => {
it('should track events', () => {
const fixture = TestBed.createComponent(Dashboard);
const analytics = TestBed.inject(Analytics);
fixture.detectChanges();
expect(analytics.track).toHaveBeenCalledWith('dashboard_viewed');
});
});Testing Async/Await
import { describe, it, expect, vi } from 'vitest';
describe('User', () => {
it('should load user data', async () => {
const mockUser = { id: '1', name: 'Test' };
const httpMock = TestBed.inject(HttpTestingController);
const service = TestBed.inject(User);
const userPromise = service.loadUser('1');
httpMock.expectOne('/api/users/1').flush(mockUser);
const user = await userPromise;
expect(user).toEqual(mockUser);
});
});Coverage Configuration
// vite.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: [
'node_modules/',
'src/test-setup.ts',
'**/*.spec.ts',
'**/*.d.ts',
],
thresholds: {
statements: 80,
branches: 80,
functions: 80,
lines: 80,
},
},
},
});Vitest UI Mode
# Run with UI
npx vitest --ui
# Open UI at specific port
npx vitest --ui --port 51204Concurrent Tests
import { describe, it, expect } from 'vitest';
// Run tests in this describe block concurrently
describe.concurrent('API calls', () => {
it('should fetch users', async () => {
// ...
});
it('should fetch products', async () => {
// ...
});
it('should fetch orders', async () => {
// ...
});
});Test Fixtures
import { describe, it, expect, beforeEach } from 'vitest';
// Shared test fixtures
const createTestUser = (overrides = {}) => ({
id: '1',
name: 'Test User',
email: 'test@example.com',
...overrides,
});
const createTestProduct = (overrides = {}) => ({
id: '1',
name: 'Test Product',
price: 99.99,
...overrides,
});
describe('Order', () => {
it('should calculate total', () => {
const fixture = TestBed.createComponent(Order);
fixture.componentRef.setInput('user', createTestUser());
fixture.componentRef.setInput('products', [
createTestProduct({ price: 10 }),
createTestProduct({ id: '2', price: 20 }),
]);
fixture.detectChanges();
expect(fixture.componentInstance.total()).toBe(30);
});
});Component Harnesses
Use Angular CDK component harnesses for more maintainable tests:
Creating a Harness
import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
export class CounterHarn extends ComponentHarness {
static hostSelector = 'app-counter';
// Locators
private getIncrementButton = this.locatorFor('button.increment');
private getDecrementButton = this.locatorFor('button.decrement');
private getCountDisplay = this.locatorFor('.count');
// Actions
async increment(): Promise<void> {
const button = await this.getIncrementButton();
await button.click();
}
async decrement(): Promise<void> {
const button = await this.getDecrementButton();
await button.click();
}
// Queries
async getCount(): Promise<number> {
const display = await this.getCountDisplay();
const text = await display.text();
return parseInt(text, 10);
}
// Filter factory
static with(options: { count?: number } = {}): HarnessPredicate<CounterHarn> {
return new HarnessPredicate(CounterHarn, options)
.addOption('count', options.count, async (harness, count) => {
return (await harness.getCount()) === count;
});
}
}Using Harnesses in Tests
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
describe('Counter with Harness', () => {
let loader: HarnessLoader;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Counter],
}).compileComponents();
const fixture = TestBed.createComponent(Counter);
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should increment count', async () => {
const counter = await loader.getHarness(CounterHarn);
expect(await counter.getCount()).toBe(0);
await counter.increment();
expect(await counter.getCount()).toBe(1);
await counter.increment();
expect(await counter.getCount()).toBe(2);
});
it('should find counter with specific count', async () => {
const counter = await loader.getHarness(CounterHarn);
await counter.increment();
await counter.increment();
// Find counter with count of 2
const counterWith2 = await loader.getHarness(CounterHarn.with({ count: 2 }));
expect(counterWith2).toBeTruthy();
});
});Testing Router
RouterTestingHarness
import { RouterTestingHarness } from '@angular/router/testing';
import { provideRouter } from '@angular/router';
describe('Router Navigation', () => {
let harness: RouterTestingHarness;
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
provideRouter([
{ path: '', component: Home },
{ path: 'users/:id', component: UserCmpt },
]),
],
}).compileComponents();
harness = await RouterTestingHarness.create();
});
it('should navigate to user page', async () => {
const component = await harness.navigateByUrl('/users/123', UserCmpt);
expect(component.id()).toBe('123');
});
it('should display user name', async () => {
await harness.navigateByUrl('/users/123');
expect(harness.routeNativeElement?.textContent).toContain('User 123');
});
});Testing Guards
describe('AuthGuard', () => {
let authService: jasmine.SpyObj<Auth>;
beforeEach(() => {
authService = jasmine.createSpyObj('Auth', ['isAuthenticated']);
TestBed.configureTestingModule({
providers: [
{ provide: Auth, useValue: authService },
provideRouter([
{ path: 'login', component: Login },
{
path: 'dashboard',
component: Dashboard,
canActivate: [authGuard],
},
]),
],
});
});
it('should allow access when authenticated', async () => {
authService.isAuthenticated.and.returnValue(true);
const harness = await RouterTestingHarness.create();
await harness.navigateByUrl('/dashboard');
expect(harness.routeNativeElement?.textContent).toContain('Dashboard');
});
it('should redirect to login when not authenticated', async () => {
authService.isAuthenticated.and.returnValue(false);
const harness = await RouterTestingHarness.create();
await harness.navigateByUrl('/dashboard');
expect(TestBed.inject(Router).url).toBe('/login');
});
});Testing Forms
Testing Signal Forms
import { form, FormField, required, email } from '@angular/forms/signals';
@Component({
imports: [FormField],
template: `
<form (submit)="onSubmit($event)">
<input [formField]="loginForm.email" />
<input [formField]="loginForm.password" type="password" />
<button type="submit" [disabled]="loginForm().invalid()">Submit</button>
</form>
`,
})
export class Login {
model = signal({ email: '', password: '' });
loginForm = form(this.model, (schemaPath) => {
required(schemaPath.email);
email(schemaPath.email);
required(schemaPath.password);
});
submitted = signal(false);
onSubmit(event: Event) {
event.preventDefault();
if (this.loginForm().valid()) {
this.submitted.set(true);
}
}
}
describe('Login', () => {
let fixture: ComponentFixture<Login>;
let component: Login;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Login],
}).compileComponents();
fixture = TestBed.createComponent(Login);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be invalid when empty', () => {
expect(component.loginForm().invalid()).toBeTrue();
});
it('should be valid with correct data', () => {
component.model.set({
email: 'test@example.com',
password: 'password123',
});
expect(component.loginForm().valid()).toBeTrue();
});
it('should show email error for invalid email', () => {
component.loginForm.email().value.set('invalid');
fixture.detectChanges();
expect(component.loginForm.email().invalid()).toBeTrue();
expect(component.loginForm.email().errors().some(e => e.kind === 'email')).toBeTrue();
});
it('should disable submit button when invalid', () => {
const button = fixture.nativeElement.querySelector('button');
expect(button.disabled).toBeTrue();
});
});Testing Reactive Forms
describe('ReactiveForm', () => {
it('should validate form', () => {
const fixture = TestBed.createComponent(ProfileForm);
const component = fixture.componentInstance;
expect(component.form.valid).toBeFalse();
component.form.patchValue({
name: 'John',
email: 'john@example.com',
});
expect(component.form.valid).toBeTrue();
});
it('should show validation errors', () => {
const fixture = TestBed.createComponent(ProfileForm);
fixture.detectChanges();
const emailControl = fixture.componentInstance.form.controls.email;
emailControl.setValue('invalid');
emailControl.markAsTouched();
fixture.detectChanges();
const errorElement = fixture.nativeElement.querySelector('.error');
expect(errorElement.textContent).toContain('Invalid email');
});
});Testing Directives
Attribute Directive
@Directive({
selector: '[appHighlight]',
host: {
'[style.backgroundColor]': 'color()',
},
})
export class Highlight {
color = input('yellow', { alias: 'appHighlight' });
}
describe('Highlight', () => {
@Component({
imports: [Highlight],
template: `<p appHighlight="lightblue">Test</p>`,
})
class Test {}
it('should apply background color', () => {
const fixture = TestBed.createComponent(Test);
fixture.detectChanges();
const p = fixture.nativeElement.querySelector('p');
expect(p.style.backgroundColor).toBe('lightblue');
});
});Structural Directive
@Directive({
selector: '[appIf]',
})
export class If {
private templateRef = inject(TemplateRef);
private viewContainer = inject(ViewContainerRef);
condition = input.required<boolean>({ alias: 'appIf' });
constructor() {
effect(() => {
if (this.condition()) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
});
}
}
describe('If', () => {
@Component({
imports: [If],
template: `<p *appIf="show()">Visible</p>`,
})
class TestCmpt {
show = signal(false);
}
it('should show content when condition is true', () => {
const fixture = TestBed.createComponent(Test);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('p')).toBeNull();
fixture.componentInstance.show.set(true);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('p')).toBeTruthy();
});
});Testing Pipes
@Pipe({ name: 'truncate' })
export class Truncate implements PipeTransform {
transform(value: string, length: number = 50): string {
if (value.length <= length) return value;
return value.substring(0, length) + '...';
}
}
describe('Truncate', () => {
let pipe: Truncate;
beforeEach(() => {
pipe = new Truncate();
});
it('should not truncate short strings', () => {
expect(pipe.transform('Hello', 10)).toBe('Hello');
});
it('should truncate long strings', () => {
expect(pipe.transform('Hello World', 5)).toBe('Hello...');
});
it('should use default length', () => {
const longString = 'a'.repeat(60);
const result = pipe.transform(longString);
expect(result.length).toBe(53); // 50 + '...'
});
});E2E Testing Setup
Playwright Configuration
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:4200',
trace: 'on-first-retry',
},
webServer: {
command: 'npm run start',
url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI,
},
});E2E Test Example
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Login', () => {
test('should login successfully', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Welcome');
});
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'wrong@example.com');
await page.fill('input[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
await expect(page.locator('.error')).toBeVisible();
await expect(page.locator('.error')).toContainText('Invalid credentials');
});
});Test Utilities
Custom Test Helpers
// test-utils.ts
export function setSignalInput<T>(
fixture: ComponentFixture<any>,
inputName: string,
value: T
): void {
fixture.componentRef.setInput(inputName, value);
fixture.detectChanges();
}
export async function waitForSignal<T>(
signal: () => T,
predicate: (value: T) => boolean,
timeout = 5000
): Promise<T> {
const start = Date.now();
while (Date.now() - start < timeout) {
const value = signal();
if (predicate(value)) return value;
await new Promise(resolve => setTimeout(resolve, 10));
}
throw new Error('Timeout waiting for signal');
}
// Usage
it('should load data', async () => {
const fixture = TestBed.createComponent(Data);
fixture.detectChanges();
await waitForSignal(
() => fixture.componentInstance.data(),
data => data !== undefined
);
expect(fixture.componentInstance.data()).toBeDefined();
});Vitest Setup and Migration Guide
Vitest vs Jasmine Comparison
| Feature | Vitest | Jasmine/Karma |
|---|---|---|
| Speed | Faster (native ESM) | Slower |
| Watch mode | Instant feedback | Slower rebuilds |
| Mocking | vi.fn(), vi.mock() | jasmine.createSpy() |
| Assertions | expect() (Chai-style) | expect() (Jasmine) |
| UI | Built-in UI mode | Karma browser |
| Config | angular.json | karma.conf.js |
Migration from Jasmine to Vitest
Spy Migration
// Jasmine
const spy = jasmine.createSpy('callback');
spy.and.returnValue('value');
expect(spy).toHaveBeenCalledWith('arg');
// Vitest
const spy = vi.fn();
spy.mockReturnValue('value');
expect(spy).toHaveBeenCalledWith('arg');SpyOn Migration
// Jasmine
spyOn(service, 'method').and.returnValue(of(data));
// Vitest
vi.spyOn(service, 'method').mockReturnValue(of(data));createSpyObj Migration
// Jasmine
const mockService = jasmine.createSpyObj('UserService', ['getUser', 'updateUser']);
mockService.getUser.and.returnValue(of({ id: '1', name: 'Test' }));
// Vitest
const mockService = {
getUser: vi.fn(),
updateUser: vi.fn(),
};
mockService.getUser.mockReturnValue(of({ id: '1', name: 'Test' }));Async Testing Migration
// Jasmine - using done callback
it('should load data', (done) => {
service.loadData().subscribe(data => {
expect(data).toBeDefined();
done();
});
});
// Vitest - using async/await
it('should load data', async () => {
const data = await firstValueFrom(service.loadData());
expect(data).toBeDefined();
});Clock/Timer Migration
// Jasmine
jasmine.clock().install();
jasmine.clock().tick(1000);
jasmine.clock().uninstall();
// Vitest
vi.useFakeTimers();
vi.advanceTimersByTime(1000);
vi.useRealTimers();Vitest Configuration Details
Full angular.json Configuration
{
"projects": {
"your-app": {
"architect": {
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json",
"buildTarget": "your-app:build"
}
}
}
}
}
}tsconfig.spec.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["vitest/globals"]
},
"include": ["src/**/*.spec.ts"]
}Optional vite.config.ts
For advanced configuration, create a vite.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
include: ['src/**/*.spec.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: [
'node_modules/',
'src/test-setup.ts',
'**/*.spec.ts',
'**/*.d.ts',
],
},
},
});Running Vitest
# Run tests
ng test
# Watch mode
ng test --watch
# Coverage
ng test --code-coverage
# Run specific file pattern
ng test --include='**/user*.spec.ts'
# CI mode (single run)
ng test --watch=falseRelated skills
How it compares
Use angular-testing for Vitest unit and harness specs; pair with angular-di when tests need facade or injection-token mocking strategies.
FAQ
Should I use Vitest or Jasmine for Angular v20+?
Vitest is recommended; Angular v20+ has native Vitest support through the @angular/build package.
How do I test OnPush components with signal inputs?
Use fixture.componentRef.setInput for signal inputs, then call fixture.detectChanges after each update.
Does this skill cover E2E browser tests?
No. Use Cypress or Playwright skills separately; this skill covers unit and integration tests only.
Is Angular Testing safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.