---
title: "How To Test Components In Angular Using TestBed"
date: "2023-01-16"
slug: "how-to-test-components-in-angular-using-testbed"
author: "Dany Paredes"
canonical: "https://danywalls.com/how-to-test-components-in-angular-using-testbed"
description: "Learn how to unit test Angular components with TestBed, handle child components and service mocks, and switch from Karma/Jasmine to modern Vitest with Standalone and Signals."
---


> ⚡ **Using Modern Angular (v17+ & Vitest)?** Jump directly to [Modern Component Testing with Standalone, Signals & Vitest](#modern-component-testing-with-standalone-signals--vitest) to see the idiomatic, lightning-fast test setup.

When talking about unit testing, the classic textbook definition is to test the smallest isolated units of an app, like pure functions or isolated classes. That sounds simple in theory, but in real-world frontend development, components have templates, dependency injection, lifecycle hooks, signal bindings, and child components.

If you don't know how to set up `TestBed` properly, your tests will quickly throw missing provider errors (`NullInjectorError`) or unknown element errors (`NG0304`).

In this practical guide, I will show you how to test Angular components step by step: configuring `TestBed`, mocking injected services, isolating child components safely, and transitioning from legacy Jasmine/Karma setups to modern **Vitest**.

---

## The Scenario: An NBA Player List Component

Let's imagine we have a component that displays a list of NBA players fetched from an API via `PlayerService`. It delegates rendering each player card to a child component `<app-player>`.

Here is the component implementation in both legacy and modern syntax:

### 1. Legacy Angular (v2 – v16: NgModule & Constructor DI)

```typescript
// player-list.component.ts (Legacy)
import { Component, OnInit } from '@angular/core';
import { PlayerService } from '../services/player.service';
import { Player } from '../models/player';

@Component({
  selector: 'app-player-list',
  templateUrl: './player-list.component.html',
})
export class PlayerListComponent implements OnInit {
  players: Player[] = [];

  constructor(private playerService: PlayerService) {}

  ngOnInit(): void {
    this.playerService.getPlayers().subscribe((res) => {
      this.players = res.data;
    });
  }
}
```

```html
<!-- player-list.component.html (Legacy) -->
<div *ngFor="let player of players" class="player-card">
  <app-player [player]="player"></app-player>
</div>
```

---

### 2. Modern Angular (v17+ / v19: Standalone, Signals & inject())

```typescript
// player-list.component.ts (Modern Standalone & Signals)
import { Component, inject, OnInit, signal } from '@angular/core';
import { PlayerService } from '../services/player.service';
import { Player } from '../models/player';
import { PlayerComponent } from '../player/player.component';

@Component({
  selector: 'app-player-list',
  standalone: true,
  imports: [PlayerComponent],
  template: `
    @for (player of players(); track player.id) {
      <div class="player-card">
        <app-player [player]="player" />
      </div>
    } @empty {
      <p>No players found.</p>
    }
  `,
})
export class PlayerListComponent implements OnInit {
  private playerService = inject(PlayerService);
  players = signal<Player[]>([]);

  ngOnInit(): void {
    this.playerService.getPlayers().subscribe((res) => {
      this.players.set(res.data);
    });
  }
}
```

Now let's see how we configure `TestBed` to test this component without making real HTTP requests or breaking child templates.

---

## What is TestBed and How Does It Work?

`TestBed` is Angular's primary testing API. It creates a dynamic Angular testing module (`DynamicTestModule`) that mimics an Angular environment in memory.

The core workflow consists of two phases:
1. `TestBed.configureTestingModule(...)`: Configures the imports, providers, and component under test.
2. `TestBed.createComponent(Component)`: Instantiates the component and returns a `ComponentFixture`.

The `ComponentFixture` gives you access to:
- `fixture.componentInstance`: The TypeScript component class instance (for checking properties, signals, and methods).
- `fixture.nativeElement`: The root HTML DOM element of the component.
- `fixture.debugElement`: Angular's abstraction over the DOM with querying helpers (`query(By.css(...))`).
- `fixture.detectChanges()`: Manually triggers change detection and runs template bindings.

---

## Step-by-Step: Testing with Legacy Jasmine & Karma

Let's review the traditional Jasmine setup first so you understand how dependency injection errors happen.

### Step 1: Basic TestBed Setup

```typescript
// player-list.component.spec.ts (Jasmine)
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PlayerListComponent } from './player-list.component';
import { PlayerService } from '../services/player.service';

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [PlayerListComponent],
      providers: [
        // If we don't provide PlayerService here, we get NullInjectorError!
      ],
    }).compileComponents();

    fixture = TestBed.createComponent(PlayerListComponent);
    component = fixture.componentInstance;
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
```

If you run this test, it will fail with:
```bash
NullInjectorError: No provider for PlayerService!
```

### Step 2: Providing a Mock Service with Jasmine Spies

To prevent Angular from creating the real service (which might try to make real HTTP calls), provide a mock using `jasmine.createSpyObj`:

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { PlayerListComponent } from './player-list.component';
import { PlayerService } from '../services/player.service';

describe('PlayerListComponent with Mock Service', () => {
  let fixture: ComponentFixture<PlayerListComponent>;
  let mockPlayerService: jasmine.SpyObj<PlayerService>;

  const FAKE_PLAYERS = {
    data: [
      { id: 1, name: 'LeBron James' },
      { id: 2, name: 'Stephen Curry' },
    ],
  };

  beforeEach(async () => {
    mockPlayerService = jasmine.createSpyObj<PlayerService>('PlayerService', ['getPlayers']);
    mockPlayerService.getPlayers.and.returnValue(of(FAKE_PLAYERS));

    await TestBed.configureTestingModule({
      declarations: [PlayerListComponent],
      providers: [
        { provide: PlayerService, useValue: mockPlayerService },
      ],
    }).compileComponents();

    fixture = TestBed.createComponent(PlayerListComponent);
  });

  it('should fetch players on ngOnInit', () => {
    fixture.detectChanges(); // Triggers ngOnInit()

    expect(mockPlayerService.getPlayers).toHaveBeenCalledTimes(1);
    expect(fixture.componentInstance.players.length).toBe(2);
  });
});
```

### Step 3: Handling the Child Component (`<app-player>`)

When you run `fixture.detectChanges()`, Angular parses the HTML template and encounters `<app-player>`. If it's not declared in the test module, you'll see:
```bash
Error: NG0304: 'app-player' is not a known element
```

**Never use `NO_ERRORS_SCHEMA` to fix this!** It disables template validation completely and hides typos. Instead, declare a lightweight stub component:

```typescript
import { Component, Input } from '@angular/core';
import { By } from '@angular/platform-browser';

// 1. Lightweight stub component
@Component({
  selector: 'app-player',
  template: '<div class="player-stub">{{ player?.name }}</div>',
})
class MockPlayerComponent {
  @Input() player: any;
}

// 2. Add to declarations
beforeEach(async () => {
  await TestBed.configureTestingModule({
    declarations: [PlayerListComponent, MockPlayerComponent],
    providers: [{ provide: PlayerService, useValue: mockPlayerService }],
  }).compileComponents();
});

it('should render two player cards in the DOM', () => {
  fixture.detectChanges();

  const renderedPlayers = fixture.debugElement.queryAll(By.css('.player-stub'));
  expect(renderedPlayers.length).toBe(2);
});
```

---

## Modern Component Testing with Standalone, Signals & Vitest

In modern Angular (v18 and v19+), **Vitest** is supported natively via the `@angular/build` test builder. Vitest runs tests directly in Node with ESM without spinning up a heavy Chrome browser instance, making test suites run in fractions of a second.

Here is how you test a modern Standalone component with Signals using Vitest:

```typescript
// player-list.component.spec.ts (Modern Angular + Vitest)
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, input } from '@angular/core';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { PlayerListComponent } from './player-list.component';
import { PlayerService } from '../services/player.service';
import { Player } from '../models/player';

// 1. Modern Standalone Mock Component using Signal inputs
@Component({
  selector: 'app-player',
  standalone: true,
  template: '<div class="player-stub">{{ player().name }}</div>',
})
class MockPlayerComponent {
  player = input.required<Player>();
}

describe('PlayerListComponent (Vitest + Standalone)', () => {
  let fixture: ComponentFixture<PlayerListComponent>;
  let component: PlayerListComponent;

  // 2. Mock service using Vitest vi.fn()
  const mockPlayerService = {
    getPlayers: vi.fn(),
  };

  const MOCK_DATA = {
    data: [
      { id: 1, name: 'Luka Dončić' },
      { id: 2, name: 'Giannis Antetokounmpo' },
    ],
  };

  beforeEach(async () => {
    vi.clearAllMocks();
    mockPlayerService.getPlayers.mockReturnValue(of(MOCK_DATA));

    await TestBed.configureTestingModule({
      // Standalone components go in 'imports', NOT 'declarations'
      imports: [PlayerListComponent],
      providers: [
        { provide: PlayerService, useValue: mockPlayerService },
      ],
    })
    // Override the real child component with our mock
    .overrideComponent(PlayerListComponent, {
      remove: { imports: [] },
      add: { imports: [MockPlayerComponent] },
    })
    .compileComponents();

    fixture = TestBed.createComponent(PlayerListComponent);
    component = fixture.componentInstance;
  });

  it('should instantiate the standalone component', () => {
    expect(component).toBeTruthy();
  });

  it('should populate the players signal on init', () => {
    fixture.detectChanges();

    expect(mockPlayerService.getPlayers).toHaveBeenCalledTimes(1);
    // Reading signal values directly in assertions
    expect(component.players().length).toBe(2);
    expect(component.players()[0].name).toBe('Luka Dončić');
  });

  it('should render the list of players in the DOM with @for', () => {
    fixture.detectChanges();

    const playerNodes = fixture.debugElement.queryAll(By.css('.player-stub'));
    expect(playerNodes.length).toBe(2);
    expect(playerNodes[0].nativeElement.textContent).toContain('Luka Dončić');
  });
});
```

---

## How to Test Signal Inputs (`input()` and `model()`)

If you want to test a child component that accepts a Signal `input()`, you don't need complex wrapper components anymore. You can update signal inputs directly via `fixture.componentRef.setInput`:

```typescript
// player.component.spec.ts (Testing Signal Inputs with Vitest)
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from 'vitest';
import { PlayerComponent } from './player.component';

describe('PlayerComponent (Signal Inputs)', () => {
  let fixture: ComponentFixture<PlayerComponent>;
  let component: PlayerComponent;

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

    fixture = TestBed.createComponent(PlayerComponent);
    component = fixture.componentInstance;
  });

  it('should render updated player details when input signal changes', () => {
    // Set input signal directly
    fixture.componentRef.setInput('player', { id: 10, name: 'Nikola Jokić' });
    fixture.detectChanges();

    const nameElement = fixture.nativeElement.querySelector('.player-name');
    expect(nameElement.textContent).toContain('Nikola Jokić');
  });
});
```

---

## Jasmine vs Vitest Comparison Cheat Sheet

| Feature | Legacy Jasmine & Karma | Modern Vitest |
| :--- | :--- | :--- |
| **Execution Engine** | Headless Chrome browser (slow start) | Native Node.js ESM (sub-second execution) |
| **Mock Functions** | `jasmine.createSpy()`, `spyOn()` | `vi.fn()`, `vi.spyOn()` |
| **Clear Mocks** | `jasmine.restoreAllMocks()` | `vi.clearAllMocks()`, `vi.resetAllMocks()` |
| **Async Timers** | `jasmine.clock().tick(100)` | `vi.advanceTimersByTime(100)` |
| **Standalone Setup** | `declarations: [MyComp]` | `imports: [MyComp]` |
| **Input Testing** | `component.myInput = val` | `fixture.componentRef.setInput('myInput', val)` |

---

## Conclusion

Testing Angular components with `TestBed` gives you full control over change detection, dependency injection, and DOM assertions.

By migrating from legacy `NgModule` declarations and Jasmine/Karma to **Standalone components and Vitest**, you eliminate browser overhead, get instant feedback in your terminal, and write clean tests for Signal inputs and modern control flow.

