# Add Component Unit Tests

> 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.

- Skill: `brianmtreese/add-component-unit-tests` (Agent Skill)
- Install (CLI): `npx skillmds@latest add brianmtreese/add-component-unit-tests`
- Raw SKILL.md: https://api.skillmd.com/api/skills/brianmtreese/add-component-unit-tests/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: brianmtreese (https://skillmd.com/u/brianmtreese)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/brianmtreese/add-component-unit-tests

---


# 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:

1. **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()`)
2. **Derive the test plan** using the [Test Plan Heuristics](#test-plan-heuristics) below. Briefly list the cases you will cover before writing code.
3. **Write the spec file** at `<component-folder>/<kebab-name>.component.spec.ts` using the [Spec Template](#spec-template).
4. **Follow the [Best Practices](#best-practices)** — one behavior per test, arrange-act-assert, no snapshot tests for signals, no testing of private implementation details.
5. **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.

```ts
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`:

```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`:

```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.

