Add Angular Component Unit Tests
Creates a <name>.component.spec.ts file next to an Angular component that covers its public API and rendered behavior using Angular's TestBed and Jasmine.
Assumptions
- Angular v20+ standalone components (no
NgModule, no standalone: true).
- Inputs/outputs declared with
input() / input.required() / output().
- State with
signal() and derived state with computed().
- Native control flow (
@if, @for, @switch) in templates.
- Host bindings/listeners declared via the
host object.
ChangeDetectionStrategy.OnPush.
Workflow
Follow these steps in order:
- Read the component file to build an inventory of:
- Inputs (required vs optional, default values)
- Outputs (payload types)
- Signals and computed values (including initial values and derivation logic)
- Methods (public and
protected) — what they mutate, emit, or return
- Host bindings/listeners in the
host object
- Template elements that render conditionally, iterate, or respond to events
- Injected dependencies (via
inject())
- Derive the test plan using the Test Plan Heuristics below. Briefly list the cases you will cover before writing code.
- Write the spec file at
<component-folder>/<kebab-name>.component.spec.ts using the Spec Template.
- Follow the Best Practices — one behavior per test, arrange-act-assert, no snapshot tests for signals, no testing of private implementation details.
- Do not modify the component to make it testable unless the user asks. If something is genuinely untestable, flag it instead of refactoring silently.
Test Plan Heuristics
For each component, generate tests from this checklist. Skip categories that don't apply.
- Creation: component compiles and renders with required inputs set.
- Inputs:
- Required inputs: render reflects the provided value.
- Optional inputs: render reflects the default when omitted, and the override when provided.
- Input changes propagate (update via
componentRef.setInput(...), then fixture.detectChanges()).
- Outputs: each
output() emits with the expected payload when its trigger fires (user interaction or method call). Use spyOn on emit or subscribe to the output.
- Signals / computed:
- Initial value is correct.
- Value updates when dependencies change.
- Computed values are derived correctly for representative inputs (include an edge case — empty, null, boundary).
- Methods: each method produces its documented side effect (signal update, output emission, DOM change after
detectChanges).
- Template:
@if branches — both truthy and falsy paths render correctly.
@for — empty, one item, many items; track doesn't cause duplicates.
@switch — each case renders its expected block.
- Event bindings wire to the correct method/output.
- Class/style bindings reflect the correct state.
- Host: host classes, attributes, and listeners behave correctly.
- Dependencies: injected services are replaced with fakes/spies via
providers in TestBed.configureTestingModule.
Spec Template
Use this as the starting point. Adapt to the component under test.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { <PascalName>Component } from './<kebab-name>.component';
describe('<PascalName>Component', () => {
let fixture: ComponentFixture<<PascalName>Component>;
let component: <PascalName>Component;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [<PascalName>Component],
}).compileComponents();
fixture = TestBed.createComponent(<PascalName>Component);
component = fixture.componentInstance;
// Set required inputs before the first detectChanges.
fixture.componentRef.setInput('<requiredInput>', '<value>');
fixture.detectChanges();
});
it('creates', () => {
expect(component).toBeTruthy();
});
describe('inputs', () => {
it('renders the <requiredInput> value', () => {
const el = fixture.debugElement.query(By.css('<selector>')).nativeElement;
expect(el.textContent).toContain('<value>');
});
it('reflects updates to <requiredInput>', () => {
fixture.componentRef.setInput('<requiredInput>', '<newValue>');
fixture.detectChanges();
const el = fixture.debugElement.query(By.css('<selector>')).nativeElement;
expect(el.textContent).toContain('<newValue>');
});
});
describe('outputs', () => {
it('emits <outputName> when <trigger>', () => {
const emitSpy = spyOn(component.<outputName>, 'emit');
const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement;
button.click();
expect(emitSpy).toHaveBeenCalledOnceWith(<expectedPayload>);
});
});
describe('<computedOrSignalName>', () => {
it('returns <expected> for <scenario>', () => {
fixture.componentRef.setInput('<input>', <value>);
fixture.detectChanges();
expect(component.<computedOrSignalName>()).toBe(<expected>);
});
});
describe('template', () => {
it('shows <elementA> when <condition>', () => {
fixture.componentRef.setInput('<input>', <truthyValue>);
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('<selectorA>'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('<selectorB>'))).toBeNull();
});
});
});
Best Practices
- One behavior per test. Each
it asserts a single observable outcome.
- Arrange / Act / Assert — keep the three phases visually separated.
- Test public behavior, not implementation. Drive the component through inputs and DOM events; assert on outputs, rendered DOM, and public signals. Never test
private members directly.
- Use
setInput, not field assignment. Signal inputs must be set via fixture.componentRef.setInput(name, value).
- Call
fixture.detectChanges() after every state change that should affect the view.
- Query the DOM with
By.css and assert against textContent, attributes, or element presence — not on stringified HTML.
- Stub dependencies. Replace injected services with jasmine spy objects via
{ provide: X, useValue: ... } in providers.
- Async. Use
fakeAsync + tick for timers, await fixture.whenStable() for promises. Prefer them over setTimeout in tests.
- No snapshot/
toMatchSnapshot for templates — write explicit assertions.
- Deterministic. No reliance on real dates, random values, or network. Inject or freeze those.
- Descriptive names.
it('emits selected with the row id when the row is clicked'), not it('works').
- Keep
beforeEach minimal — only shared setup. Anything scenario-specific goes inside the it.
Example
Given src/app/hello-world/hello-world.component.ts:
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
@Component({
selector: 'app-hello-world',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="hello-world">
<h2>Hello World</h2>
<p>Today's date: {{ todaysDate() }}</p>
<button type="button" (click)="updateTime.emit()">Update time</button>
</section>
`,
})
export class HelloWorldComponent {
readonly todaysDate = input.required<string>();
readonly updateTime = output<void>();
}
Produce src/app/hello-world/hello-world.component.spec.ts:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HelloWorldComponent } from './hello-world.component';
describe('HelloWorldComponent', () => {
let fixture: ComponentFixture<HelloWorldComponent>;
let component: HelloWorldComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HelloWorldComponent],
}).compileComponents();
fixture = TestBed.createComponent(HelloWorldComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('todaysDate', 'Monday, January 1, 2025');
fixture.detectChanges();
});
it('creates', () => {
expect(component).toBeTruthy();
});
describe('inputs', () => {
it('renders the provided todaysDate', () => {
const text = fixture.debugElement.query(By.css('p')).nativeElement.textContent as string;
expect(text).toContain("Today's date: Monday, January 1, 2025");
});
it('reflects updates to todaysDate', () => {
fixture.componentRef.setInput('todaysDate', 'Tuesday, January 2, 2025');
fixture.detectChanges();
const text = fixture.debugElement.query(By.css('p')).nativeElement.textContent as string;
expect(text).toContain('Tuesday, January 2, 2025');
});
});
describe('outputs', () => {
it('emits updateTime when the button is clicked', () => {
const emitSpy = spyOn(component.updateTime, 'emit');
const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement;
button.click();
expect(emitSpy).toHaveBeenCalledTimes(1);
});
});
describe('template', () => {
it('renders a heading, a date line, and an update button', () => {
expect(fixture.debugElement.query(By.css('h2')).nativeElement.textContent).toContain('Hello World');
expect(fixture.debugElement.query(By.css('p'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('button')).nativeElement.textContent).toContain('Update time');
});
});
});
Don'ts
- Don't assign signal inputs directly (
component.foo = ...) — use setInput.
- Don't use
TestBed.overrideComponent just to swap a template.
- Don't test
private or protected members by casting to any.
- Don't assert on CSS selectors that include Angular-generated attributes (
_ngcontent-...).
- Don't write a single giant
it that exercises every behavior.
- Don't re-test framework behavior (that
@Input works, that output() emits at all) — test your component's use of it.
- Don't modify the component under test to make it easier to test without asking first.
1---2name: add-component-unit-tests3description: Evaluates an Angular standalone component and generates unit tests that cover its inputs, outputs, signals, computed values, methods, and template behavior following unit-testing best practices. Use when the user asks to add, generate, scaffold, or write unit tests for an Angular component.4---56# Add Angular Component Unit Tests78Creates a `<name>.component.spec.ts` file next to an Angular component that covers its public API and rendered behavior using Angular's `TestBed` and Jasmine.910## Assumptions1112- Angular v20+ standalone components (no `NgModule`, no `standalone: true`).13- Inputs/outputs declared with `input()` / `input.required()` / `output()`.14- State with `signal()` and derived state with `computed()`.15- Native control flow (`@if`, `@for`, `@switch`) in templates.16- Host bindings/listeners declared via the `host` object.17- `ChangeDetectionStrategy.OnPush`.1819## Workflow2021Follow these steps in order:22231. **Read the component file** to build an inventory of:24 - Inputs (required vs optional, default values)25 - Outputs (payload types)26 - Signals and computed values (including initial values and derivation logic)27 - Methods (public and `protected`) — what they mutate, emit, or return28 - Host bindings/listeners in the `host` object29 - Template elements that render conditionally, iterate, or respond to events30 - Injected dependencies (via `inject()`)312. **Derive the test plan** using the [Test Plan Heuristics](#test-plan-heuristics) below. Briefly list the cases you will cover before writing code.323. **Write the spec file** at `<component-folder>/<kebab-name>.component.spec.ts` using the [Spec Template](#spec-template).334. **Follow the [Best Practices](#best-practices)** — one behavior per test, arrange-act-assert, no snapshot tests for signals, no testing of private implementation details.345. **Do not modify the component** to make it testable unless the user asks. If something is genuinely untestable, flag it instead of refactoring silently.3536## Test Plan Heuristics3738For each component, generate tests from this checklist. Skip categories that don't apply.3940- **Creation**: component compiles and renders with required inputs set.41- **Inputs**:42 - Required inputs: render reflects the provided value.43 - Optional inputs: render reflects the default when omitted, and the override when provided.44 - Input changes propagate (update via `componentRef.setInput(...)`, then `fixture.detectChanges()`).45- **Outputs**: each `output()` emits with the expected payload when its trigger fires (user interaction or method call). Use `spyOn` on `emit` or subscribe to the output.46- **Signals / computed**:47 - Initial value is correct.48 - Value updates when dependencies change.49 - Computed values are derived correctly for representative inputs (include an edge case — empty, null, boundary).50- **Methods**: each method produces its documented side effect (signal update, output emission, DOM change after `detectChanges`).51- **Template**:52 - `@if` branches — both truthy and falsy paths render correctly.53 - `@for` — empty, one item, many items; `track` doesn't cause duplicates.54 - `@switch` — each case renders its expected block.55 - Event bindings wire to the correct method/output.56 - Class/style bindings reflect the correct state.57- **Host**: host classes, attributes, and listeners behave correctly.58- **Dependencies**: injected services are replaced with fakes/spies via `providers` in `TestBed.configureTestingModule`.5960## Spec Template6162Use this as the starting point. Adapt to the component under test.6364```ts65import { ComponentFixture, TestBed } from '@angular/core/testing';66import { By } from '@angular/platform-browser';67import { <PascalName>Component } from './<kebab-name>.component';6869describe('<PascalName>Component', () => {70 let fixture: ComponentFixture<<PascalName>Component>;71 let component: <PascalName>Component;7273 beforeEach(async () => {74 await TestBed.configureTestingModule({75 imports: [<PascalName>Component],76 }).compileComponents();7778 fixture = TestBed.createComponent(<PascalName>Component);79 component = fixture.componentInstance;80 // Set required inputs before the first detectChanges.81 fixture.componentRef.setInput('<requiredInput>', '<value>');82 fixture.detectChanges();83 });8485 it('creates', () => {86 expect(component).toBeTruthy();87 });8889 describe('inputs', () => {90 it('renders the <requiredInput> value', () => {91 const el = fixture.debugElement.query(By.css('<selector>')).nativeElement;92 expect(el.textContent).toContain('<value>');93 });9495 it('reflects updates to <requiredInput>', () => {96 fixture.componentRef.setInput('<requiredInput>', '<newValue>');97 fixture.detectChanges();98 const el = fixture.debugElement.query(By.css('<selector>')).nativeElement;99 expect(el.textContent).toContain('<newValue>');100 });101 });102103 describe('outputs', () => {104 it('emits <outputName> when <trigger>', () => {105 const emitSpy = spyOn(component.<outputName>, 'emit');106 const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement;107 button.click();108 expect(emitSpy).toHaveBeenCalledOnceWith(<expectedPayload>);109 });110 });111112 describe('<computedOrSignalName>', () => {113 it('returns <expected> for <scenario>', () => {114 fixture.componentRef.setInput('<input>', <value>);115 fixture.detectChanges();116 expect(component.<computedOrSignalName>()).toBe(<expected>);117 });118 });119120 describe('template', () => {121 it('shows <elementA> when <condition>', () => {122 fixture.componentRef.setInput('<input>', <truthyValue>);123 fixture.detectChanges();124 expect(fixture.debugElement.query(By.css('<selectorA>'))).not.toBeNull();125 expect(fixture.debugElement.query(By.css('<selectorB>'))).toBeNull();126 });127 });128});129```130131## Best Practices132133- **One behavior per test.** Each `it` asserts a single observable outcome.134- **Arrange / Act / Assert** — keep the three phases visually separated.135- **Test public behavior, not implementation.** Drive the component through inputs and DOM events; assert on outputs, rendered DOM, and public signals. Never test `private` members directly.136- **Use `setInput`, not field assignment.** Signal inputs must be set via `fixture.componentRef.setInput(name, value)`.137- **Call `fixture.detectChanges()`** after every state change that should affect the view.138- **Query the DOM with `By.css`** and assert against `textContent`, attributes, or element presence — not on stringified HTML.139- **Stub dependencies.** Replace injected services with jasmine spy objects via `{ provide: X, useValue: ... }` in `providers`.140- **Async.** Use `fakeAsync` + `tick` for timers, `await fixture.whenStable()` for promises. Prefer them over `setTimeout` in tests.141- **No snapshot/`toMatchSnapshot`** for templates — write explicit assertions.142- **Deterministic.** No reliance on real dates, random values, or network. Inject or freeze those.143- **Descriptive names.** `it('emits selected with the row id when the row is clicked')`, not `it('works')`.144- **Keep `beforeEach` minimal** — only shared setup. Anything scenario-specific goes inside the `it`.145146## Example147148**Given** `src/app/hello-world/hello-world.component.ts`:149150```ts151import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';152153@Component({154 selector: 'app-hello-world',155 changeDetection: ChangeDetectionStrategy.OnPush,156 template: `157 <section class="hello-world">158 <h2>Hello World</h2>159 <p>Today's date: {{ todaysDate() }}</p>160 <button type="button" (click)="updateTime.emit()">Update time</button>161 </section>162 `,163})164export class HelloWorldComponent {165 readonly todaysDate = input.required<string>();166 readonly updateTime = output<void>();167}168```169170**Produce** `src/app/hello-world/hello-world.component.spec.ts`:171172```ts173import { ComponentFixture, TestBed } from '@angular/core/testing';174import { By } from '@angular/platform-browser';175import { HelloWorldComponent } from './hello-world.component';176177describe('HelloWorldComponent', () => {178 let fixture: ComponentFixture<HelloWorldComponent>;179 let component: HelloWorldComponent;180181 beforeEach(async () => {182 await TestBed.configureTestingModule({183 imports: [HelloWorldComponent],184 }).compileComponents();185186 fixture = TestBed.createComponent(HelloWorldComponent);187 component = fixture.componentInstance;188 fixture.componentRef.setInput('todaysDate', 'Monday, January 1, 2025');189 fixture.detectChanges();190 });191192 it('creates', () => {193 expect(component).toBeTruthy();194 });195196 describe('inputs', () => {197 it('renders the provided todaysDate', () => {198 const text = fixture.debugElement.query(By.css('p')).nativeElement.textContent as string;199 expect(text).toContain("Today's date: Monday, January 1, 2025");200 });201202 it('reflects updates to todaysDate', () => {203 fixture.componentRef.setInput('todaysDate', 'Tuesday, January 2, 2025');204 fixture.detectChanges();205 const text = fixture.debugElement.query(By.css('p')).nativeElement.textContent as string;206 expect(text).toContain('Tuesday, January 2, 2025');207 });208 });209210 describe('outputs', () => {211 it('emits updateTime when the button is clicked', () => {212 const emitSpy = spyOn(component.updateTime, 'emit');213 const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement;214 button.click();215 expect(emitSpy).toHaveBeenCalledTimes(1);216 });217 });218219 describe('template', () => {220 it('renders a heading, a date line, and an update button', () => {221 expect(fixture.debugElement.query(By.css('h2')).nativeElement.textContent).toContain('Hello World');222 expect(fixture.debugElement.query(By.css('p'))).not.toBeNull();223 expect(fixture.debugElement.query(By.css('button')).nativeElement.textContent).toContain('Update time');224 });225 });226});227```228229## Don'ts230231- Don't assign signal inputs directly (`component.foo = ...`) — use `setInput`.232- Don't use `TestBed.overrideComponent` just to swap a template.233- Don't test `private` or `protected` members by casting to `any`.234- Don't assert on CSS selectors that include Angular-generated attributes (`_ngcontent-...`).235- Don't write a single giant `it` that exercises every behavior.236- Don't re-test framework behavior (that `@Input` works, that `output()` emits at all) — test your component's use of it.237- Don't modify the component under test to make it easier to test without asking first.