Automate 21risk tasks via Rube MCP (Composio). Always search tools first for current schemas.
中文简介正在准备中。当前显示原文说明。
Write unit and integration tests for Angular v20+ applications using Vitest or Jasmine with TestBed and modern testing patterns. Use for testing components with signals, OnPush change detection, services with inject(), and HTTP interactions. Triggers on test creation, testing signal-based components, mocking dependencies, or setting up test infrastructure. Don't use for E2E testing with Cypress or Playwright, or for testing non-Angular JavaScript/TypeScript code.
中文简介正在准备中。当前显示原文说明。
安装前可以查看交给 AI 代理的指令内容。
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.
npm install -D vitest jsdom
Configure 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 coverage
For Vitest migration from Jasmine and advanced configuration, see references/vitest-migration.md.
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');
});
});
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);
});
});
@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);
});
});
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');
});
});
@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);
});
});
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);
});
});
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');
});
});
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();
});
@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);
});
});
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
}));
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();
});
}));
@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.
还没有评价。欢迎分享你的使用感受。
无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。
还没有评论。来写第一条吧。
Automate 21risk tasks via Rube MCP (Composio). Always search tools first for current schemas.
中文简介正在准备中。当前显示原文说明。
Automate 2chat tasks via Rube MCP (Composio). Always search tools first for current schemas.
中文简介正在准备中。当前显示原文说明。
Automate Ably tasks via Rube MCP (Composio). Always search tools first for current schemas.
中文简介正在准备中。当前显示原文说明。
Automate Abstract tasks via Rube MCP (Composio). Always search tools first for current schemas.
中文简介正在准备中。当前显示原文说明。
Automate Abuselpdb tasks via Rube MCP (Composio). Always search tools first for current schemas.
中文简介正在准备中。当前显示原文说明。
Automate Abyssale tasks via Rube MCP (Composio). Always search tools first for current schemas.
中文简介正在准备中。当前显示原文说明。