From harness-claude
Tests Angular components, services, directives, and pipes using TestBed, ComponentFixture, fakeAsync, service mocks, HttpClientTestingModule, and spectator. For unit tests with async code and standalone components.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Test Angular components, services, directives, and pipes with TestBed, ComponentFixture, fakeAsync, and service mocks
Writes unit and integration tests for Angular v20+ apps using Vitest or Jasmine with TestBed. Covers signal-based components, OnPush change detection, inject() services, and HTTP interactions.
Implements Angular dependency injection using providers (useClass, useValue, useFactory), injectors, and services for modular, testable applications.
Guides Angular app architecture: standalone components, NgModules, Signals, NgRx, RxJS patterns, lazy loading, dependency injection, Jasmine/Jest testing.
Share bugs, ideas, or general feedback.
Test Angular components, services, directives, and pipes with TestBed, ComponentFixture, fakeAsync, and service mocks
fakeAsync + tickspectator to reduce TestBed boilerplate for component testsTestBed.configureTestingModule({ imports: [MyStandaloneComponent] }) for standalone components — import, not declare.{ provide: MyService, useValue: mockService } in the providers array. Define mocks as jasmine.createSpyObj (Jasmine) or jest.fn() (Jest) objects.fixture.detectChanges() after setup and after state mutations to trigger change detection before asserting on the DOM.fakeAsync + tick() for timer-based code. Use fakeAsync + flush() for promise-based code. Use fakeAsync + flushMicrotasks() for microtasks.fixture.debugElement.query(By.css('selector')) or fixture.nativeElement.querySelector(). Prefer By.css — it returns a DebugElement with Angular context.EventEmitter or signal output directly: component.myOutput.subscribe(spy).HttpClientTestingModule and HttpTestingController to verify requests and flush mock responses.spectator library (@ngneat/spectator) to reduce boilerplate — it wraps TestBed and adds ergonomic query helpers.// product-card.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { signal } from '@angular/core';
import { ProductCardComponent } from './product-card.component';
import { CartService } from '../cart.service';
describe('ProductCardComponent', () => {
let component: ProductCardComponent;
let fixture: ComponentFixture<ProductCardComponent>;
let mockCartService: jasmine.SpyObj<CartService>;
beforeEach(async () => {
mockCartService = jasmine.createSpyObj('CartService', ['addItem']);
await TestBed.configureTestingModule({
imports: [ProductCardComponent], // standalone component
providers: [{ provide: CartService, useValue: mockCartService }],
}).compileComponents();
fixture = TestBed.createComponent(ProductCardComponent);
component = fixture.componentInstance;
// Set required signal input
fixture.componentRef.setInput('product', {
id: '1',
name: 'Widget',
price: 9.99,
});
fixture.detectChanges();
});
it('should display the product name', () => {
const heading = fixture.debugElement.query(By.css('h2'));
expect(heading.nativeElement.textContent).toBe('Widget');
});
it('should emit addToCart when button clicked', () => {
const addSpy = jasmine.createSpy();
component.addToCart.subscribe(addSpy);
fixture.debugElement.query(By.css('button')).nativeElement.click();
expect(addSpy).toHaveBeenCalledWith({ id: '1', name: 'Widget', price: 9.99 });
});
});
// product.service.spec.ts — HTTP service test
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ProductService } from './product.service';
describe('ProductService', () => {
let service: ProductService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [ProductService],
});
service = TestBed.inject(ProductService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => httpMock.verify()); // assert no pending requests
it('should fetch products', () => {
const mockProducts = [{ id: '1', name: 'Widget', price: 9.99 }];
let result: Product[] | undefined;
service.getProducts().subscribe((p) => (result = p));
const req = httpMock.expectOne('/api/products');
expect(req.request.method).toBe('GET');
req.flush(mockProducts);
expect(result).toEqual(mockProducts);
});
});
// async test with fakeAsync
import { fakeAsync, tick } from '@angular/core/testing';
it('should debounce search input', fakeAsync(() => {
component.searchControl.setValue('widget');
tick(300); // advance timer by 300ms (debounce time)
fixture.detectChanges();
expect(mockSearchService.search).toHaveBeenCalledWith('widget');
}));
// spectator usage — reduces boilerplate significantly
import { createComponentFactory, Spectator } from '@ngneat/spectator';
describe('ProductCardComponent', () => {
let spectator: Spectator<ProductCardComponent>;
const createComponent = createComponentFactory({
component: ProductCardComponent,
mocks: [CartService],
});
beforeEach(() => {
spectator = createComponent({ props: { product: mockProduct } });
});
it('shows product name', () => {
expect(spectator.query('h2')).toHaveText('Widget');
});
it('calls addItem on click', () => {
spectator.click('button');
expect(spectator.inject(CartService).addItem).toHaveBeenCalled();
});
});
Setting signal inputs in tests: Signal inputs (input()) cannot be set directly via component.myInput = value because they are read-only signals. Use fixture.componentRef.setInput('inputName', value) instead — this is the supported API for setting signal inputs in tests.
compileComponents() requirement: In tests that use templateUrl or styleUrls, call await TestBed.configureTestingModule({...}).compileComponents() to compile the external resources asynchronously. For inline templates, it is not strictly required but harmless.
Testing OnPush components: OnPush components only update when inputs change, an async pipe resolves, or signals emit. In tests, fixture.detectChanges() triggers a change detection cycle. If you mutate state without changing a signal or input reference, the template won't update. Set signal values with .set() and call fixture.detectChanges() afterward.
Test isolation: Each TestBed.configureTestingModule call in beforeEach creates a fresh Angular testing environment. Avoid sharing mutable state across tests — reset spies in afterEach if reused.
Avoiding real HTTP calls: Always provide HttpClientTestingModule or mock the service. HttpTestingController.verify() in afterEach ensures no unexpected HTTP requests were made.
Signal stores in tests: Provide a test version of the store or override state with patchState if the store is provided in the component.
https://angular.dev/guide/testing