install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/angular-testing-patterns" ~/.claude/skills/intense-visions-harness-engineering-angular-testing-patterns-5068a2 && rm -rf "$T"
manifest:
agents/skills/codex/angular-testing-patterns/SKILL.mdsource content
Angular Testing Patterns
Test Angular components, services, directives, and pipes with TestBed, ComponentFixture, fakeAsync, and service mocks
When to Use
- Writing unit tests for Angular components with shallow or deep rendering
- Mocking service dependencies in component and service tests
- Testing asynchronous code (HTTP, timers, observables) with
+fakeAsynctick - Using
to reducespectator
boilerplate for component testsTestBed - Testing standalone components without NgModule ceremony
Instructions
- Use
for standalone components — import, not declare.TestBed.configureTestingModule({ imports: [MyStandaloneComponent] }) - Mock services with
in the{ provide: MyService, useValue: mockService }
array. Define mocks asproviders
(Jasmine) orjasmine.createSpyObj
(Jest) objects.jest.fn() - Always call
after setup and after state mutations to trigger change detection before asserting on the DOM.fixture.detectChanges() - Use
+fakeAsync
for timer-based code. Usetick()
+fakeAsync
for promise-based code. Useflush()
+fakeAsync
for microtasks.flushMicrotasks() - Query DOM elements with
orfixture.debugElement.query(By.css('selector'))
. Preferfixture.nativeElement.querySelector()
— it returns aBy.css
with Angular context.DebugElement - Test outputs by subscribing to the
or signal output directly:EventEmitter
.component.myOutput.subscribe(spy) - For HTTP services, use
andHttpClientTestingModule
to verify requests and flush mock responses.HttpTestingController - Use the
library (spectator
) to reduce boilerplate — it wraps@ngneat/spectator
and adds ergonomic query helpers.TestBed
// 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(); }); });
Details
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.
requirement: In tests that use compileComponents()
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
components: OnPush
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.
Source
https://angular.dev/guide/testing
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.