Skip to content

Write Angular Unit Tests

Analyze $ARGUMENTS and write comprehensive unit tests.

Steps

  1. Read the source file at the provided path.
  2. Read the existing .spec.ts file if one exists.
  3. Read the project's CLAUDE.md for testing conventions, test runner (Jest or Jasmine), and state management approach.
  4. Identify what needs to be tested based on the type of Angular artifact.
  5. Write or update the .spec.ts file.

What to Test by Type

Components

  • Component creation
  • Input binding (set inputs, verify template renders correctly)
  • Output emission (trigger actions, verify output emits expected values)
  • Signal and computed state (verify derived state updates correctly)
  • Template rendering with @if, @for, @switch branches
  • User interactions (click, input, form submission)
  • Conditional template elements (all branches)
  • Injected service interactions (mock services, verify calls)
  • Page components: verify service methods are called, verify data flows to child views
  • View components: verify inputs render correctly, verify outputs emit on user action

Services (Signal-Based)

  • Public methods (call method, verify signal/state changes)
  • HTTP calls (mock with HttpTestingController, test success and error)
  • Signal state transitions (initial -> loading -> loaded/error)
  • Computed properties (verify derived values)
  • Edge cases (empty data, null values, error responses)

NgRx State

  • Reducers: test as pure functions — pass initial state + action, assert new state
  • Selectors: test with .projector() or by composing mock state objects
  • Effects: test with provideMockActions, mock API services, verify dispatched actions for success and error paths

Pipes

  • Transform with typical input
  • Transform with edge cases (null, undefined, empty string)
  • Transform with various argument combinations

Directives

  • Directive applied to host element
  • Input changes reflected in DOM
  • Event handling

Testing Patterns

Component test setup

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [MyComponent],
      providers: [
        // For Jest: use jest.fn() or manual mock objects
        { provide: MyService, useValue: { loadItems: jest.fn(), items: signal([]), loading: signal(false) } }
        // For Jasmine: use jasmine.createSpyObj
        // { provide: MyService, useValue: jasmine.createSpyObj('MyService', ['loadItems'], { items: signal([]), loading: signal(false) }) }
      ]
    }).compileComponents();

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

Service test setup with HTTP

typescript
describe('MyService', () => {
  let service: MyService;
  let httpTesting: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()]
    });
    service = TestBed.inject(MyService);
    httpTesting = TestBed.inject(HttpTestingController);
  });

  afterEach(() => httpTesting.verify());
});

Setting component inputs in tests

typescript
// For signal inputs, use componentRef.setInput
fixture.componentRef.setInput('user', { id: '1', name: 'Test' });
fixture.detectChanges();

NgRx reducer test

typescript
describe('MyReducer', () => {
  it('should return initial state', () => {
    const state = myReducer(undefined, { type: 'unknown' });
    expect(state).toEqual(initialState);
  });

  it('should handle loadItems', () => {
    const state = myReducer(initialState, MyActions.loadItems());
    expect(state.loading).toBe(true);
  });

  it('should handle loadItemsSuccess', () => {
    const items = [{ id: '1', name: 'Test' }];
    const state = myReducer(initialState, MyActions.loadItemsSuccess({ items }));
    expect(selectAll(state)).toEqual(items);
    expect(state.loading).toBe(false);
  });

  it('should handle loadItemsFailure', () => {
    const state = myReducer(initialState, MyActions.loadItemsFailure({ error: 'fail' }));
    expect(state.error).toBe('fail');
    expect(state.loading).toBe(false);
  });
});

NgRx selector test

typescript
describe('MySelectors', () => {
  const mockState: MyState = {
    ids: ['1'],
    entities: { '1': { id: '1', name: 'Test' } },
    loading: false,
    error: null
  };

  it('should select all items', () => {
    const result = MySelectors.selectAllItems.projector(mockState);
    expect(result).toEqual([{ id: '1', name: 'Test' }]);
  });

  it('should select loading', () => {
    const result = MySelectors.selectLoading.projector(mockState);
    expect(result).toBe(false);
  });
});

NgRx effect test (Jest)

typescript
describe('MyEffects', () => {
  let effects: MyEffects;
  let actions$: Observable<Action>;
  let api: jest.Mocked<MyApiService>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        MyEffects,
        provideMockActions(() => actions$),
        { provide: MyApiService, useValue: { getItems: jest.fn() } }
      ]
    });
    effects = TestBed.inject(MyEffects);
    api = TestBed.inject(MyApiService) as jest.Mocked<MyApiService>;
  });

  it('should dispatch loadItemsSuccess on success', (done) => {
    const items = [{ id: '1', name: 'Test' }];
    api.getItems.mockReturnValue(of(items));
    actions$ = of(MyActions.loadItems());

    effects.loadItems$.subscribe(action => {
      expect(action).toEqual(MyActions.loadItemsSuccess({ items }));
      done();
    });
  });

  it('should dispatch loadItemsFailure on error', (done) => {
    api.getItems.mockReturnValue(throwError(() => new Error('fail')));
    actions$ = of(MyActions.loadItems());

    effects.loadItems$.pipe(take(1)).subscribe(action => {
      expect(action).toEqual(MyActions.loadItemsFailure({ error: 'fail' }));
      done();
    });
  });
});

NgRx effect test (Jasmine)

typescript
describe('MyEffects', () => {
  let effects: MyEffects;
  let actions$: Observable<Action>;
  let api: jasmine.SpyObj<MyApiService>;

  beforeEach(() => {
    api = jasmine.createSpyObj('MyApiService', ['getItems']);

    TestBed.configureTestingModule({
      providers: [
        MyEffects,
        provideMockActions(() => actions$),
        { provide: MyApiService, useValue: api }
      ]
    });
    effects = TestBed.inject(MyEffects);
  });

  it('should dispatch loadItemsSuccess on success', (done) => {
    const items = [{ id: '1', name: 'Test' }];
    api.getItems.and.returnValue(of(items));
    actions$ = of(MyActions.loadItems());

    effects.loadItems$.subscribe(action => {
      expect(action).toEqual(MyActions.loadItemsSuccess({ items }));
      done();
    });
  });
});

Rules

  • Place the test file next to the source file (e.g., my.component.spec.ts beside my.component.ts)
  • Mock all injected services; do not test service internals from component tests
  • Use jest.fn() (Jest) or jasmine.createSpyObj (Jasmine) with signal properties for service mocks
  • Use fixture.componentRef.setInput() to set signal inputs in tests
  • Call fixture.detectChanges() after state changes to update the template
  • Test observable and signal behavior, not implementation details
  • Avoid any type in tests; use proper types for mocks
  • Do not add unnecessary async/await unless testing async behavior
  • Keep test descriptions specific: "should display user name when user input is provided"
  • For NgRx reducers: test every on() handler with the corresponding action
  • For NgRx effects: test both success and error paths; verify the correct action is dispatched

Output

After writing tests, summarize:

  • Test file location
  • Number of test cases written
  • What behaviors are covered
  • Any edge cases or scenarios that are difficult to test automatically

Copyright © MSG Systems Romania AI Labs