This guide covers testing practices for the SoundHub project, including unit tests (Jest), E2E tests (Playwright), and common patterns for mocking Angular dependencies.
# Run all unit tests in the workspace
npx nx run-many --target=test --all
# Run tests for all affected projects (based on git changes)
npx nx affected --target=test
# Run all tests in a specific library
npx nx test <library-name>
# Run specific test files
npx nx test <library-name> --testFile="component.spec.ts"
# Run tests in watch mode
npx nx test <library-name> --watch
# Run tests with coverage
npx nx test <library-name> --coverage
# Run E2E tests
npx nx e2e e2e
# Run E2E tests in headed mode (see browser)
npx nx e2e e2e --headedimport { ComponentFixture, TestBed } from '@angular/core/testing';
import { YourComponent } from './your.component';
describe('YourComponent', () => {
let component: YourComponent;
let fixture: ComponentFixture<YourComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [YourComponent], // Standalone components
providers: [
// Provide mocks here
],
}).compileComponents();
fixture = TestBed.createComponent(YourComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
});RouterLink directives, the Router mock must include all methods and observables that RouterLink depends on:
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
let mockRouter: jest.Mocked<Partial<Router>>;
beforeEach(async () => {
mockRouter = {
navigate: jest.fn(),
createUrlTree: jest.fn().mockReturnValue({}),
serializeUrl: jest.fn().mockReturnValue(''),
events: new Subject(), // Required for RouterLink
} as unknown as jest.Mocked<Router>;
await TestBed.configureTestingModule({
imports: [YourComponent],
providers: [
{ provide: Router, useValue: mockRouter },
],
}).compileComponents();
});Required Router methods:
navigate()- For programmatic navigationcreateUrlTree()- Used by RouterLink directive to create URL treesserializeUrl()- Used by RouterLink to serialize URLsevents- Observable stream of router events (useSubjectfor testing)
Common Router Testing Scenarios:
// Test navigation was called
it('should navigate on action', () => {
component.goToPage();
expect(mockRouter.navigate).toHaveBeenCalledWith(['/path', 'param']);
});
// Test RouterLink in template (requires full mock)
it('should render router links', () => {
fixture.detectChanges();
const link = fixture.nativeElement.querySelector('a[routerLink]');
expect(link).toBeTruthy();
});import { ActivatedRoute } from '@angular/router';
let mockActivatedRoute: Partial<ActivatedRoute>;
beforeEach(async () => {
mockActivatedRoute = {
snapshot: {
paramMap: {
get: jest.fn((key: string) => {
if (key === 'id') return 'test-id';
return null;
}),
},
},
} as any;
await TestBed.configureTestingModule({
imports: [YourComponent],
providers: [
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
],
}).compileComponents();
});import { of, throwError } from 'rxjs';
let mockService: jest.Mocked<YourService>;
beforeEach(async () => {
mockService = {
getData: jest.fn().mockReturnValue(of({ id: 1, name: 'Test' })),
postData: jest.fn().mockReturnValue(of(void 0)),
} as unknown as jest.Mocked<YourService>;
await TestBed.configureTestingModule({
imports: [YourComponent],
providers: [
{ provide: YourService, useValue: mockService },
],
}).compileComponents();
});
// Test success case
it('should load data', () => {
component.loadData();
expect(mockService.getData).toHaveBeenCalled();
expect(component.data()).toBe('Test');
});
// Test error case
it('should handle error', () => {
mockService.getData.mockReturnValue(throwError(() => new Error('Failed')));
component.loadData();
expect(component.error()).toBeTruthy();
});it('should update signal value', () => {
expect(component.count()).toBe(0);
component.increment();
expect(component.count()).toBe(1);
});
it('should react to computed signals', () => {
component.count.set(5);
expect(component.doubleCount()).toBe(10);
});import { ReactiveFormsModule } from '@angular/forms';
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [YourComponent, ReactiveFormsModule],
}).compileComponents();
});
it('should validate form', () => {
const form = component.form;
form.patchValue({ name: '', email: 'invalid' });
expect(form.valid).toBe(false);
form.patchValue({ name: 'John', email: 'john@example.com' });
expect(form.valid).toBe(true);
});
it('should submit valid form', () => {
const spy = jest.spyOn(component, 'onSubmit');
component.form.patchValue({ name: 'John' });
component.onSubmit();
expect(spy).toHaveBeenCalled();
expect(mockService.save).toHaveBeenCalledWith({ name: 'John' });
});import { test, expect } from '@playwright/test';
test.describe('Feature Name', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should perform user action', async ({ page }) => {
await page.click('button[aria-label="Action"]');
await expect(page.locator('.result')).toBeVisible();
});
});- Use semantic selectors: Prefer
role,aria-label,data-testidover CSS classes - Wait for conditions: Use Playwright's auto-waiting and explicit waits
- Test user flows: Focus on complete user journeys, not implementation details
- Keep tests independent: Each test should be runnable in isolation
// Good: semantic selector
await page.click('button[aria-label="Add Preset"]');
// Avoid: implementation-specific selector
await page.click('.preset-list__add-btn');
// Good: wait for condition
await expect(page.locator('.preset-item')).toHaveCount(3);
// Avoid: arbitrary timeouts
await page.waitForTimeout(1000);# Run all tests
dotnet test
# Run tests with coverage
dotnet test /p:CollectCoverage=truepublic class ServiceTests
{
private readonly Mock<IDependency> _mockDependency;
private readonly Service _service;
public ServiceTests()
{
_mockDependency = new Mock<IDependency>();
_service = new Service(_mockDependency.Object);
}
[Fact]
public async Task Method_Should_ReturnExpectedResult()
{
// Arrange
_mockDependency.Setup(x => x.GetData())
.ReturnsAsync(new Data());
// Act
var result = await _service.ProcessData();
// Assert
Assert.NotNull(result);
_mockDependency.Verify(x => x.GetData(), Times.Once);
}
}Cause: RouterLink directive trying to access router.events which is undefined.
Solution: Add events: new Subject() to your Router mock (see Mocking Angular Router section).
Cause: RouterLink directive requires createUrlTree method.
Solution: Add createUrlTree: jest.fn().mockReturnValue({}) to your Router mock.
Cause: RouterLink directive requires serializeUrl method.
Solution: Add serializeUrl: jest.fn().mockReturnValue('') to your Router mock.
- Ensure all dependencies are installed
- Check for timing-sensitive tests (use proper waiting strategies)
- Verify environment variables are set correctly
- Check for hardcoded paths or URLs
- Test behavior, not implementation: Focus on what the component does, not how it does it
- Keep tests simple: One assertion per test when possible
- Use descriptive names: Test names should describe the expected behavior
- Arrange-Act-Assert: Structure tests with clear setup, action, and verification phases
- Avoid test interdependence: Each test should run independently
- Use TestBed for integration tests: Test components with their dependencies
- Mock external dependencies: Services, HTTP, Router, etc.
- Test signals reactivity: Verify computed values update correctly
- Test template interactions: Use
fixture.detectChanges()and query elements - Verify lifecycle hooks: Test
ngOnInit,ngOnDestroy, etc.
- Aim for meaningful coverage, not just high percentages
- Focus on critical paths and business logic
- Don't test framework code or simple getters/setters
- Use coverage reports to identify untested edge cases
# Generate coverage report
npx nx test feature --coverage