AngularJSComponent Testing

Component Testing in Angular

Component testing in Angular uses TestBed to create a mini Angular environment for your component — compiling its template, wiring up dependencies, and enabling interaction testing.

Good component tests verify behavior visible to the user, not implementation details.

TestBed Basics

TS
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { GreetingComponent } from './greeting.component';

describe('GreetingComponent', () => {
  let component: GreetingComponent;
  let fixture: ComponentFixture<GreetingComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [GreetingComponent], // For standalone components
      // declarations: [GreetingComponent], // For module-based components
    }).compileComponents();

    fixture = TestBed.createComponent(GreetingComponent);
    component = fixture.componentInstance;
    fixture.detectChanges(); // Trigger ngOnInit + initial change detection
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
Note
For standalone components, use imports in the testing module. For module-based components, use declarations. You don't need compileComponents() for standalone components.
Querying the DOM

Access the rendered DOM via fixture.nativeElement or Angular's By utility:

TS
import { By } from '@angular/platform-browser';

// Native DOM queries
const heading = fixture.nativeElement.querySelector('h1');
const buttons = fixture.nativeElement.querySelectorAll('button');
const text = fixture.nativeElement.textContent;

// Angular's By.css (returns DebugElement — richer API)
const debugEl = fixture.debugElement.query(By.css('.submit-btn'));
const allItems = fixture.debugElement.queryAll(By.css('li'));

// Access component via DebugElement
const btnComponent = fixture.debugElement.query(
  By.directive(ButtonComponent)
).componentInstance;
Testing a Simple Component

TS
// greeting.component.ts
import { Component, Input } from '@angular/core';

@Component({
  standalone: true,
  selector: 'app-greeting',
  template: `
    <h1>Hello, {{ name }}!</h1>
    <p class="subtitle">Welcome back</p>
  `,
})
export class GreetingComponent {
  @Input() name = 'World';
}

TS
// greeting.component.spec.ts
describe('GreetingComponent', () => {
  let component: GreetingComponent;
  let fixture: ComponentFixture<GreetingComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [GreetingComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(GreetingComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('shows default greeting', () => {
    const h1 = fixture.nativeElement.querySelector('h1');
    expect(h1.textContent).toBe('Hello, World!');
  });

  it('shows custom name via Input', () => {
    component.name = 'Alice';
    fixture.detectChanges();

    const h1 = fixture.nativeElement.querySelector('h1');
    expect(h1.textContent).toBe('Hello, Alice!');
  });

  it('renders subtitle text', () => {
    const subtitle = fixture.nativeElement.querySelector('.subtitle');
    expect(subtitle.textContent).toBe('Welcome back');
  });
});
Testing User Interactions

TS
// counter.component.ts
@Component({
  standalone: true,
  selector: 'app-counter',
  template: `
    <p data-testid="count">Count: {{ count }}</p>
    <button (click)="increment()">+</button>
    <button (click)="decrement()">-</button>
    <button (click)="reset()">Reset</button>
  `,
})
export class CounterComponent {
  count = 0;

  increment() { this.count++; }
  decrement() { this.count--; }
  reset() { this.count = 0; }
}

TS
// counter.component.spec.ts
describe('CounterComponent', () => {
  let fixture: ComponentFixture<CounterComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [CounterComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(CounterComponent);
    fixture.detectChanges();
  });

  function getCount(): string {
    return fixture.nativeElement.querySelector('[data-testid="count"]').textContent;
  }

  function clickButton(text: string) {
    const buttons = fixture.nativeElement.querySelectorAll('button');
    const btn = Array.from(buttons).find((b: any) => b.textContent.includes(text));
    (btn as HTMLElement).click();
    fixture.detectChanges();
  }

  it('starts at 0', () => {
    expect(getCount()).toContain('Count: 0');
  });

  it('increments on + click', () => {
    clickButton('+');
    expect(getCount()).toContain('Count: 1');
  });

  it('decrements on - click', () => {
    clickButton('-');
    expect(getCount()).toContain('Count: -1');
  });

  it('resets to 0', () => {
    clickButton('+');
    clickButton('+');
    clickButton('Reset');
    expect(getCount()).toContain('Count: 0');
  });
});
Testing with Service Dependencies

TS
// user-list.component.ts
@Component({
  standalone: true,
  imports: [AsyncPipe, NgFor],
  selector: 'app-user-list',
  template: `
    @if (loading) {
      <p>Loading...</p>
    } @else {
      <ul>
        @for (user of users; track user.id) {
          <li>{{ user.name }}</li>
        }
      </ul>
    }
  `,
})
export class UserListComponent implements OnInit {
  users: User[] = [];
  loading = true;
  private userService = inject(UserService);

  ngOnInit() {
    this.userService.getUsers().subscribe({
      next: (users) => {
        this.users = users;
        this.loading = false;
      },
    });
  }
}

TS
// user-list.component.spec.ts
import { of } from 'rxjs';

describe('UserListComponent', () => {
  let fixture: ComponentFixture<UserListComponent>;
  let mockUserService: jasmine.SpyObj<UserService>;

  const mockUsers: User[] = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
  ];

  beforeEach(async () => {
    mockUserService = jasmine.createSpyObj('UserService', ['getUsers']);
    mockUserService.getUsers.and.returnValue(of(mockUsers));

    await TestBed.configureTestingModule({
      imports: [UserListComponent],
      providers: [
        { provide: UserService, useValue: mockUserService },
      ],
    }).compileComponents();

    fixture = TestBed.createComponent(UserListComponent);
    fixture.detectChanges();
  });

  it('calls getUsers on init', () => {
    expect(mockUserService.getUsers).toHaveBeenCalledTimes(1);
  });

  it('renders a list item for each user', () => {
    const items = fixture.nativeElement.querySelectorAll('li');
    expect(items.length).toBe(2);
    expect(items[0].textContent).toContain('Alice');
    expect(items[1].textContent).toContain('Bob');
  });

  it('hides loading state after data loads', () => {
    const loading = fixture.nativeElement.querySelector('p');
    expect(loading).toBeNull(); // loading element gone
  });
});
Testing Inputs and Outputs

TS
// product-card.component.ts
@Component({
  standalone: true,
  selector: 'app-product-card',
  template: `
    <div class="card">
      <h2>{{ product.name }}</h2>
      <p>{{ product.price | currency }}</p>
      <button (click)="addToCart.emit(product)">Add to Cart</button>
    </div>
  `,
})
export class ProductCardComponent {
  @Input({ required: true }) product!: { name: string; price: number; id: number };
  @Output() addToCart = new EventEmitter<{ name: string; price: number; id: number }>();
}

TS
describe('ProductCardComponent', () => {
  let fixture: ComponentFixture<ProductCardComponent>;
  let component: ProductCardComponent;

  const mockProduct = { id: 1, name: 'Laptop', price: 999.99 };

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ProductCardComponent, CurrencyPipe],
    }).compileComponents();

    fixture = TestBed.createComponent(ProductCardComponent);
    component = fixture.componentInstance;
    component.product = mockProduct;
    fixture.detectChanges();
  });

  it('displays product name', () => {
    const h2 = fixture.nativeElement.querySelector('h2');
    expect(h2.textContent).toBe('Laptop');
  });

  it('emits addToCart with product on button click', () => {
    let emittedProduct: any;
    component.addToCart.subscribe((p) => (emittedProduct = p));

    const btn = fixture.nativeElement.querySelector('button');
    btn.click();

    expect(emittedProduct).toEqual(mockProduct);
  });

  it('emits the correct product', () => {
    const spy = spyOn(component.addToCart, 'emit');
    fixture.nativeElement.querySelector('button').click();
    expect(spy).toHaveBeenCalledWith(mockProduct);
  });
});
Testing Reactive Forms

TS
// login-form.component.ts
@Component({
  standalone: true,
  imports: [ReactiveFormsModule],
  selector: 'app-login-form',
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <input formControlName="email" placeholder="Email" />
      <input type="password" formControlName="password" placeholder="Password" />
      <button type="submit" [disabled]="form.invalid">Login</button>
      @if (form.get('email')?.errors?.['required'] && form.get('email')?.touched) {
        <span class="error">Email is required</span>
      }
    </form>
  `,
})
export class LoginFormComponent {
  form = new FormGroup({
    email: new FormControl('', [Validators.required, Validators.email]),
    password: new FormControl('', [Validators.required, Validators.minLength(8)]),
  });

  submitted = false;

  onSubmit() {
    if (this.form.valid) {
      this.submitted = true;
    }
  }
}

TS
describe('LoginFormComponent', () => {
  let fixture: ComponentFixture<LoginFormComponent>;
  let component: LoginFormComponent;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [LoginFormComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(LoginFormComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('submit button is disabled when form is invalid', () => {
    const btn = fixture.nativeElement.querySelector('button[type="submit"]');
    expect(btn.disabled).toBe(true);
  });

  it('submit button is enabled with valid inputs', () => {
    component.form.setValue({ email: 'user@test.com', password: 'secret123' });
    fixture.detectChanges();

    const btn = fixture.nativeElement.querySelector('button[type="submit"]');
    expect(btn.disabled).toBe(false);
  });

  it('shows error when email touched and empty', () => {
    const emailCtrl = component.form.get('email')!;
    emailCtrl.markAsTouched();
    fixture.detectChanges();

    const error = fixture.nativeElement.querySelector('.error');
    expect(error).toBeTruthy();
    expect(error.textContent).toContain('Email is required');
  });

  it('sets submitted flag on valid submit', () => {
    component.form.setValue({ email: 'user@test.com', password: 'secret123' });
    fixture.detectChanges();

    fixture.nativeElement.querySelector('button').click();
    fixture.detectChanges();

    expect(component.submitted).toBe(true);
  });
});
Component Harnesses (Angular CDK)

Angular CDK testing harnesses provide a stable, implementation-agnostic way to interact with Angular Material (and other) components. They insulate tests from DOM changes.

TS
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';
import { MatInputHarness } from '@angular/material/input/testing';

describe('LoginComponent with harnesses', () => {
  let loader: HarnessLoader;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [LoginComponent, MatFormFieldModule, MatInputModule, MatButtonModule],
    }).compileComponents();

    const fixture = TestBed.createComponent(LoginComponent);
    loader = TestbedHarnessEnvironment.loader(fixture);
    fixture.detectChanges();
  });

  it('button is disabled initially', async () => {
    const btn = await loader.getHarness(MatButtonHarness.with({ text: 'Login' }));
    expect(await btn.isDisabled()).toBe(true);
  });

  it('enables button after valid input', async () => {
    const emailInput = await loader.getHarness(MatInputHarness.with({ placeholder: 'Email' }));
    const passInput = await loader.getHarness(MatInputHarness.with({ placeholder: 'Password' }));

    await emailInput.setValue('user@test.com');
    await passInput.setValue('secret123');

    const btn = await loader.getHarness(MatButtonHarness.with({ text: 'Login' }));
    expect(await btn.isDisabled()).toBe(false);
  });
});
Best Practices
  • Test behavior, not implementation — avoid testing private methods or component internals

  • Use data-testid attributes for stable DOM queries that survive refactoring

  • Always call fixture.detectChanges() after changing component inputs or state

  • Mock service dependencies to keep component tests isolated

  • Test the full user interaction flow, not just individual methods

  • Use ComponentFixture.whenStable() or fakeAsync/tick for async operations

  • Prefer Angular CDK harnesses for Material components — they are stable across version upgrades

Warning
Avoid querying DOM elements by CSS class names that might be used for styling — they can change. Use data-testid attributes or semantic HTML selectors (role, label) for robust queries.