Angular's testing infrastructure provides comprehensive tools for unit testing, integration testing, and end-to-end testing of components, services, and applications with TestBed, ComponentFixture, and async testing utilities.
Primary testing utility for configuring and creating test environments.
/**
* Main testing utility for configuring test environment
*/
class TestBed {
/**
* Configure testing module with metadata
* @param moduleDef - Test module configuration
* @returns TestBed instance
*/
static configureTestingModule(moduleDef: TestModuleMetadata): TestBed;
/**
* Create component instance for testing
* @param component - Component type to create
* @returns Component fixture
*/
static createComponent<T>(component: Type<T>): ComponentFixture<T>;
/**
* Inject dependency in test context
* @param token - Dependency token
* @param notFoundValue - Optional default value
* @returns Dependency instance
*/
static inject<T>(token: ProviderToken<T>, notFoundValue?: T): T;
/**
* Inject dependency with null default
* @param token - Dependency token
* @param notFoundValue - Null default value
* @returns Dependency instance or null
*/
static inject<T>(token: ProviderToken<T>, notFoundValue: null): T | null;
/**
* Run function in injection context
* @param fn - Function to run
* @returns Function result
*/
static runInInjectionContext<T>(fn: () => T): T;
/**
* Flush pending async operations
* @returns Promise that resolves when stable
*/
static flushEffects(): void;
/**
* Reset testing module configuration
*/
static resetTestingModule(): void;
/**
* Get testing module reference
* @returns Module reference
*/
static get testingModule(): NgModuleRef<any>;
/**
* Initialize test environment
* @param ngModule - Root testing module
* @param platform - Platform factory
* @param options - Test environment options
*/
static initTestEnvironment(
ngModule: Type<any> | Type<any>[],
platform: PlatformFactory,
options?: TestEnvironmentOptions
): void;
/**
* Reset test environment
*/
static resetTestEnvironment(): void;
}
/**
* Metadata for configuring test modules
*/
interface TestModuleMetadata {
/** Components, directives, and pipes to declare */
declarations?: any[];
/** Modules to import */
imports?: any[];
/** Providers for dependency injection */
providers?: any[];
/** Schemas for template compilation */
schemas?: (SchemaMetadata | any[])[];
}
/**
* Options for test environment setup
*/
interface TestEnvironmentOptions {
/** Whether to enable animations in tests */
enableAnimations?: boolean;
/** Whether to teardown test module after each test */
teardown?: ModuleTeardownOptions;
}
/**
* Options for module teardown after tests
*/
interface ModuleTeardownOptions {
/** Whether to destroy after each test */
destroyAfterEach: boolean;
/** Whether to destroy test components after each test */
rethrowErrors?: boolean;
}Utilities for testing Angular components with fixtures and debugging.
/**
* Wrapper for component under test with debugging utilities
*/
class ComponentFixture<T> {
/** Component instance being tested */
componentInstance: T;
/** Debug element wrapper around component */
debugElement: DebugElement;
/** Native DOM element of component */
nativeElement: any;
/** Change detector reference for manual triggering */
changeDetectorRef: ChangeDetectorRef;
/** NgZone instance for the fixture */
ngZone: NgZone | null;
/**
* Trigger change detection cycle
* @param checkNoChanges - Whether to check for unexpected changes
*/
detectChanges(checkNoChanges?: boolean): void;
/**
* Check for changes without updating DOM
*/
checkNoChanges(): void;
/**
* Enable/disable automatic change detection
* @param autoDetect - Whether to auto-detect changes
*/
autoDetectChanges(autoDetect?: boolean): void;
/**
* Check if fixture is stable (no pending async operations)
* @returns True if stable
*/
isStable(): boolean;
/**
* Wait for fixture to become stable
* @returns Promise that resolves when stable
*/
whenStable(): Promise<any>;
/**
* Destroy component fixture and cleanup
*/
destroy(): void;
}
/**
* Debug wrapper around DOM element for testing
*/
class DebugElement {
/** Native DOM element */
nativeElement: any;
/** Component instance if element is component */
componentInstance: any;
/** Associated directive context */
context: any;
/** Element name */
name: string;
/** Parent debug element */
parent: DebugElement | null;
/** Child debug elements */
children: DebugElement[];
/** Properties bound to element */
properties: {[key: string]: any};
/** Attributes on element */
attributes: {[key: string]: string | null};
/** CSS classes on element */
classes: {[key: string]: boolean};
/** CSS styles on element */
styles: {[key: string]: string | null};
/** Event listeners on element */
listeners: DebugEventListener[];
/**
* Query for descendant element
* @param predicate - Search predicate
* @returns First matching element or null
*/
query(predicate: Predicate<DebugElement>): DebugElement | null;
/**
* Query for multiple descendant elements
* @param predicate - Search predicate
* @returns Array of matching elements
*/
queryAll(predicate: Predicate<DebugElement>): DebugElement[];
/**
* Query for descendant by CSS selector
* @param selector - CSS selector
* @returns First matching element or null
*/
query(selector: string): DebugElement | null;
/**
* Query for multiple descendants by CSS selector
* @param selector - CSS selector
* @returns Array of matching elements
*/
queryAll(selector: string): DebugElement[];
/**
* Trigger event on element
* @param eventName - Event name
* @param eventObj - Event object
*/
triggerEventHandler(eventName: string, eventObj?: any): void;
}
/**
* Event listener for debugging
*/
interface DebugEventListener {
/** Event name */
name: string;
/** Event callback */
callback: Function;
}
/**
* Function type for element predicates
*/
type Predicate<T> = (value: T) => boolean;Utilities for testing asynchronous code with controlled time passage.
/**
* Wrap test in fake async zone for time control
* @param fn - Test function to wrap
* @returns Wrapped test function
*/
function fakeAsync(fn: Function): (...args: any[]) => any;
/**
* Simulate passage of time in fake async zone
* @param millis - Milliseconds to advance (default: 0)
* @param tickOptions - Options for tick behavior
*/
function tick(millis?: number, tickOptions?: {processNewMacroTasksSynchronously?: boolean}): void;
/**
* Flush all pending asynchronous tasks
* @returns Number of tasks flushed
*/
function flush(): number;
/**
* Flush only microtasks (Promises, queueMicrotask)
*/
function flushMicrotasks(): void;
/**
* Discard periodic tasks (setInterval, etc.)
*/
function discardPeriodicTasks(): void;
/**
* Reset fake async zone state
*/
function resetFakeAsyncZone(): void;
/**
* Get current fake async zone instance
* @returns Fake async test zone spec
*/
function getFakeAsyncZoneSpec(): any;Testing utilities for Angular's defer blocks and lazy loading.
/**
* Testing utility for defer blocks
*/
class DeferBlockFixture {
/**
* Get current state of defer block
* @returns Current defer block state
*/
getState(): DeferBlockState;
/**
* Render defer block with specified behavior
* @param behavior - Defer behavior for testing
* @returns Promise that resolves when rendering complete
*/
render(behavior: DeferBlockBehavior): Promise<void>;
}
/**
* State enumeration for defer blocks
*/
enum DeferBlockState {
/** Placeholder content is rendered */
Placeholder = 0,
/** Loading content is rendered */
Loading = 1,
/** Main content is rendered */
Complete = 2,
/** Error content is rendered */
Error = 3
}
/**
* Behavior configuration for defer block testing
*/
enum DeferBlockBehavior {
/** Render placeholder only */
Placeholder = 0,
/** Render loading content */
Loading = 1,
/** Render main content */
Complete = 2,
/** Render error content */
Error = 3
}Special providers and tokens for testing scenarios.
/**
* Token for enabling automatic change detection in component fixtures
*/
const ComponentFixtureAutoDetect: InjectionToken<boolean>;
/**
* Token for disabling NgZone in component fixtures
*/
const ComponentFixtureNoNgZone: InjectionToken<boolean>;
/**
* Get current TestBed instance
* @returns TestBed instance
*/
function getTestBed(): TestBed;
/**
* Inject dependency in test context
* @param token - Dependency token
* @param notFoundValue - Optional default value
* @returns Dependency instance
*/
function inject<T>(token: ProviderToken<T>, notFoundValue?: T): T;
/**
* Wrapper for injection setup in tests
*/
class InjectSetupWrapper {
/**
* Configure injection for test
* @param moduleDef - Module definition
* @returns Setup wrapper
*/
static setup(moduleDef: TestModuleMetadata): InjectSetupWrapper;
}
/**
* Helper for module configuration in tests
* @param moduleDef - Module definition
* @returns Test configuration
*/
function withModule(moduleDef: TestModuleMetadata): any;Additional utilities for testing scenarios and debugging.
/**
* Get debug node wrapper for DOM node
* @param node - DOM node
* @returns Debug node wrapper
*/
function getDebugNode(node: Node): DebugNode | null;
/**
* Extract native DOM elements from debug elements
* @param debugEls - Array of debug elements
* @returns Array of native elements
*/
function asNativeElements(debugEls: DebugElement[]): any[];
/**
* Base class for debug node wrappers
*/
class DebugNode {
/** Native DOM node */
nativeNode: any;
/** Parent debug node */
parent: DebugElement | null;
/** Associated directive context */
context: any;
/** Listeners attached to node */
listeners: DebugEventListener[];
}import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<h3>{{title}}</h3>
<p>Count: {{count}}</p>
<button (click)="increment()" data-test="increment-btn">+</button>
<button (click)="decrement()" data-test="decrement-btn">-</button>
</div>
`
})
class CounterComponent {
@Input() title = 'Counter';
@Input() count = 0;
@Output() countChange = new EventEmitter<number>();
increment(): void {
this.count++;
this.countChange.emit(this.count);
}
decrement(): void {
this.count--;
this.countChange.emit(this.count);
}
}
describe('CounterComponent', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CounterComponent]
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display initial count', () => {
component.count = 5;
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.textContent).toContain('Count: 5');
});
it('should increment count when button clicked', () => {
spyOn(component.countChange, 'emit');
const incrementBtn = fixture.debugElement.query('[data-test="increment-btn"]');
incrementBtn.triggerEventHandler('click', null);
expect(component.count).toBe(1);
expect(component.countChange.emit).toHaveBeenCalledWith(1);
});
it('should update DOM when count changes', () => {
component.count = 10;
fixture.detectChanges();
const countElement = fixture.nativeElement.querySelector('p');
expect(countElement.textContent).toBe('Count: 10');
});
});import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
class UserService {
constructor(private http: HttpClient) {}
getUsers() {
return this.http.get<User[]>('/api/users');
}
getUser(id: string) {
return this.http.get<User>(`/api/users/${id}`);
}
}
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should fetch users', () => {
const mockUsers = [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' }
];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
it('should fetch single user', () => {
const mockUser = { id: '1', name: 'John' };
service.getUser('1').subscribe(user => {
expect(user).toEqual(mockUser);
});
const req = httpMock.expectOne('/api/users/1');
req.flush(mockUser);
});
});import { fakeAsync, tick, flush, flushMicrotasks } from '@angular/core/testing';
@Component({
template: `
<div>
<button (click)="loadData()">Load Data</button>
<div *ngIf="loading">Loading...</div>
<div *ngIf="data">{{data}}</div>
</div>
`
})
class AsyncComponent {
loading = false;
data: string | null = null;
loadData(): void {
this.loading = true;
this.data = null;
// Simulate async operation
setTimeout(() => {
this.data = 'Loaded data';
this.loading = false;
}, 1000);
}
loadDataWithPromise(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => {
resolve('Promise data');
}, 500);
});
}
}
describe('AsyncComponent', () => {
let component: AsyncComponent;
let fixture: ComponentFixture<AsyncComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AsyncComponent]
}).compileComponents();
fixture = TestBed.createComponent(AsyncComponent);
component = fixture.componentInstance;
});
it('should load data after timeout', fakeAsync(() => {
component.loadData();
fixture.detectChanges();
// Initially should show loading
expect(component.loading).toBe(true);
expect(fixture.nativeElement.textContent).toContain('Loading...');
// Advance time by 1000ms
tick(1000);
fixture.detectChanges();
// Should show loaded data
expect(component.loading).toBe(false);
expect(component.data).toBe('Loaded data');
expect(fixture.nativeElement.textContent).toContain('Loaded data');
}));
it('should handle promise-based async', fakeAsync(() => {
let result: string | undefined;
component.loadDataWithPromise().then(data => {
result = data;
});
// Flush microtasks (Promises)
flushMicrotasks();
tick(500);
expect(result).toBe('Promise data');
}));
it('should flush all pending tasks', fakeAsync(() => {
component.loadData();
// Flush all pending async operations
flush();
fixture.detectChanges();
expect(component.loading).toBe(false);
expect(component.data).toBe('Loaded data');
}));
});import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
@Component({
template: `
<form>
<input [(ngModel)]="username" placeholder="Username" data-test="username">
<input [(ngModel)]="password" type="password" placeholder="Password" data-test="password">
<button (click)="login()" [disabled]="!isValid()" data-test="login-btn">Login</button>
</form>
<div *ngIf="error" class="error" data-test="error">{{error}}</div>
`
})
class LoginComponent {
username = '';
password = '';
error = '';
isValid(): boolean {
return this.username.length > 0 && this.password.length > 0;
}
login(): void {
if (this.username === 'admin' && this.password === 'password') {
console.log('Login successful');
} else {
this.error = 'Invalid credentials';
}
}
}
describe('LoginComponent Integration', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LoginComponent],
imports: [FormsModule]
}).compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should enable login button when form is valid', async () => {
const usernameInput = fixture.debugElement.query(By.css('[data-test="username"]'));
const passwordInput = fixture.debugElement.query(By.css('[data-test="password"]'));
const loginButton = fixture.debugElement.query(By.css('[data-test="login-btn"]'));
// Initially button should be disabled
expect(loginButton.nativeElement.disabled).toBe(true);
// Fill in form
usernameInput.nativeElement.value = 'testuser';
usernameInput.nativeElement.dispatchEvent(new Event('input'));
passwordInput.nativeElement.value = 'testpass';
passwordInput.nativeElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
// Button should now be enabled
expect(loginButton.nativeElement.disabled).toBe(false);
});
it('should show error for invalid credentials', () => {
component.username = 'invalid';
component.password = 'invalid';
fixture.detectChanges();
const loginButton = fixture.debugElement.query(By.css('[data-test="login-btn"]'));
loginButton.triggerEventHandler('click', null);
fixture.detectChanges();
const errorElement = fixture.debugElement.query(By.css('[data-test="error"]'));
expect(errorElement.nativeElement.textContent).toBe('Invalid credentials');
});
});describe('Component with Custom Providers', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
let mockService: jasmine.SpyObj<DataService>;
beforeEach(async () => {
const spy = jasmine.createSpyObj('DataService', ['getData', 'saveData']);
await TestBed.configureTestingModule({
declarations: [MyComponent],
providers: [
{ provide: DataService, useValue: spy },
{ provide: 'API_URL', useValue: 'http://test-api.com' },
// Auto change detection
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
mockService = TestBed.inject(DataService) as jasmine.SpyObj<DataService>;
});
it('should call service on init', () => {
mockService.getData.and.returnValue(of(['test data']));
component.ngOnInit();
expect(mockService.getData).toHaveBeenCalled();
});
});