or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

application-management.mdchange-detection.mdcomponent-system.mddependency-injection.mdindex.mdmodern-authoring.mdresource-api.mdrxjs-interop.mdtesting.mdutilities-helpers.md
tile.json

testing.mddocs/

Testing Utilities

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.

Capabilities

TestBed

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;
}

Component Testing

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;

Async Testing (Fake Async)

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;

Defer Block Testing

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
}

Test Injection and Providers

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;

Test Utilities

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[];
}

Usage Examples

Basic Component Testing

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');
  });
});

Service Testing with Injection

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);
  });
});

Async Testing with fakeAsync

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');
  }));
});

Component Integration Testing

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');
  });
});

Custom Test Providers

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();
  });
});