- Added comprehensive JSDoc comments to mapping functions for checkbox and text filter inputs, improving code readability and maintainability. - Refactored checkboxFilterInputMapping and checkboxOptionMapping functions to enhance clarity and structure. - Removed unused data-range-filter-input mapping files and tests to streamline the codebase. - Introduced a new dateRangeFilterInputMapping function with detailed mapping logic for date range inputs. - Updated filter input schemas to include descriptive comments for better understanding of properties. - Implemented unit tests for date range filter input mapping to ensure correct functionality. - Enhanced existing filter mapping functions with improved error handling and validation. - Updated index exports to reflect the removal and addition of mapping files.
20 KiB
Testing Guidelines
Unit Testing Requirements
- Test files should end with
.spec.ts - Use Spectator for Component, Directive and Service tests
- Use Jest as the test runner
- Follow the Arrange-Act-Assert (AAA) pattern in tests
- Mock external dependencies to isolate the unit under test
- Mock child components to ensure true unit testing isolation
Spectator Overview
Spectator is a powerful library that simplifies Angular testing by:
- Reducing boilerplate code in tests
- Providing easy DOM querying utilities
- Offering a clean API for triggering events
- Supporting testing of components, directives, and services
- Including custom matchers for clearer assertions
Best Practices
Component Testing
- Use
createComponentFactoryfor standalone components - Use
createHostFactorywhen testing components with templates and inputs/outputs - Use
createDirectiveFactoryfor testing directives - Use
createServiceFactoryfor testing services - Test component inputs, outputs, and lifecycle hooks
- Verify DOM rendering and component behavior separately
Mocking Child Components
Always mock child components to:
- Isolate the component under test
- Prevent unintended side effects
- Reduce test complexity
- Improve test performance
import { MockComponent } from 'ng-mocks';
import { ChildComponent } from './child.component';
describe('ParentComponent', () => {
const createComponent = createComponentFactory({
component: ParentComponent,
declarations: [MockComponent(ChildComponent)],
});
});
Spectator API Reference
Core Factory Methods
-
For Components:
const createComponent = createComponentFactory({ component: MyComponent, imports: [SomeModule], declarations: [SomeDirective], providers: [SomeService], componentProviders: [], // Providers specific to the component componentViewProviders: [], // ViewProviders for the component mocks: [ServiceToMock], // Automatically mocks the service detectChanges: false, // Whether to run change detection initially }); -
For Components with Host:
const createHost = createHostFactory({ component: MyComponent, template: `<app-my [prop]="value" (event)="handle()"></app-my>`, // ...other options similar to createComponentFactory }); -
For Directives:
const createDirective = createDirectiveFactory({ directive: MyDirective, template: `<div myDirective [prop]="value"></div>`, // ...other options }); -
For Services:
const createService = createServiceFactory({ service: MyService, providers: [DependencyService], mocks: [HttpClient], entryComponents: [], }); -
For HTTP Services:
const createHttpService = createHttpFactory({ service: MyHttpService, providers: [SomeService], mocks: [TokenService], });
Querying Elements
Spectator offers multiple ways to query the DOM:
// Basic CSS selectors
const button = spectator.query('button.submit');
const inputs = spectator.queryAll('input');
// By directive/component type
const childComponent = spectator.query(ChildComponent);
const directives = spectator.queryAll(MyDirective);
// Advanced text-based selectors
spectator.query(byText('Submit'));
spectator.query(byLabel('Username'));
spectator.query(byPlaceholder('Enter your email'));
spectator.query(byValue('Some value'));
spectator.query(byTitle('Click here'));
spectator.query(byAltText('Logo image'));
spectator.query(byRole('button', { pressed: true }));
// Accessing native elements
const { nativeElement } = spectator.query('.class-name');
Working with Inputs and Outputs
// Setting component inputs
spectator.setInput('username', 'JohnDoe');
spectator.setInput({
username: 'JohnDoe',
isActive: true,
});
// For host components
spectator.setHostInput('propName', value);
// Working with outputs
const outputSpy = jest.fn();
spectator.output('statusChange').subscribe(outputSpy);
// Trigger and verify outputs
spectator.click('button.submit');
expect(outputSpy).toHaveBeenCalledWith({ status: 'submitted' });
Event Triggering API
Spectator provides a rich API for simulating user interactions:
// Mouse events
spectator.click('.button');
spectator.doubleClick('#item');
spectator.hover('.tooltip');
spectator.mouseEnter('.dropdown');
spectator.mouseLeave('.dropdown');
// Keyboard events
spectator.keyboard.pressEscape();
spectator.keyboard.pressEnter();
spectator.keyboard.pressKey('A');
spectator.keyboard.pressKeys('ctrl.a');
// Form interactions
spectator.typeInElement('New value', 'input.username');
spectator.blur('input.username');
spectator.focus('input.password');
spectator.selectOption(selectEl, 'Option 2');
// Custom events
spectator.triggerEventHandler(MyComponent, 'customEvent', eventObj);
spectator.dispatchFakeEvent(element, 'mouseover');
spectator.dispatchTouchEvent(element, 'touchstart');
Example Test Structures
Basic Component Test
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { MyComponent } from './my-component.component';
describe('MyComponent', () => {
let spectator: Spectator<MyComponent>;
const createComponent = createComponentFactory(MyComponent);
beforeEach(() => {
spectator = createComponent();
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
it('should handle action correctly', () => {
// Arrange
spectator.setInput('inputProp', 'testValue');
// Act
spectator.click('button');
// Assert
expect(spectator.component.outputProp).toBe('expectedValue');
});
});
Host Component Test with Child Components
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { ParentComponent } from './parent.component';
import { ChildComponent } from './child.component';
import { MockComponent } from 'ng-mocks';
describe('ParentComponent', () => {
let spectator: SpectatorHost<ParentComponent>;
const createHost = createHostFactory({
component: ParentComponent,
declarations: [MockComponent(ChildComponent)],
template: `
<app-parent
[input]="inputValue"
(output)="handleOutput($event)">
</app-parent>`,
});
beforeEach(() => {
spectator = createHost(undefined, {
hostProps: {
inputValue: 'test',
handleOutput: jest.fn(),
},
});
});
it('should pass input to child component', () => {
// Arrange
const childComponent = spectator.query(ChildComponent);
// Assert
expect(childComponent.input).toBe('test');
});
});
Testing Events and Outputs
it('should emit when button is clicked', () => {
// Arrange
const outputSpy = jest.fn();
spectator.component.outputEvent.subscribe(outputSpy);
// Act
spectator.click('button');
// Assert
expect(outputSpy).toHaveBeenCalledWith(expectedValue);
});
Testing Services
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { UserService } from './user.service';
import { HttpClient } from '@angular/common/http';
describe('UserService', () => {
let spectator: SpectatorService<UserService>;
let httpClient: HttpClient;
const createService = createServiceFactory({
service: UserService,
mocks: [HttpClient],
});
beforeEach(() => {
spectator = createService();
httpClient = spectator.inject(HttpClient);
});
it('should fetch users', () => {
// Arrange
const mockUsers = [{ id: 1, name: 'John' }];
httpClient.get.mockReturnValue(of(mockUsers));
// Act
let result;
spectator.service.getUsers().subscribe((users) => {
result = users;
});
// Assert
expect(httpClient.get).toHaveBeenCalledWith('/api/users');
expect(result).toEqual(mockUsers);
});
});
Testing HTTP Services
import {
createHttpFactory,
HttpMethod,
SpectatorHttp,
} from '@ngneat/spectator/jest';
import { UserHttpService } from './user-http.service';
describe('UserHttpService', () => {
let spectator: SpectatorHttp<UserHttpService>;
const createHttp = createHttpFactory({
service: UserHttpService,
});
beforeEach(() => (spectator = createHttp()));
it('should call the correct API endpoint when getting users', () => {
spectator.service.getUsers().subscribe();
spectator.expectOne('/api/users', HttpMethod.GET);
});
it('should include auth token in the headers', () => {
spectator.service.getUsers().subscribe();
const req = spectator.expectOne('/api/users', HttpMethod.GET);
expect(req.request.headers.get('Authorization')).toBeTruthy();
});
});
Testing Directives
import {
createDirectiveFactory,
SpectatorDirective,
} from '@ngneat/spectator/jest';
import { HighlightDirective } from './highlight.directive';
describe('HighlightDirective', () => {
let spectator: SpectatorDirective<HighlightDirective>;
const createDirective = createDirectiveFactory({
directive: HighlightDirective,
template: `<div highlight="yellow">Testing</div>`,
});
beforeEach(() => (spectator = createDirective()));
it('should change the background color', () => {
expect(spectator.element).toHaveStyle({
backgroundColor: 'yellow',
});
});
it('should respond to mouse events', () => {
spectator.dispatchMouseEvent(spectator.element, 'mouseover');
expect(spectator.element).toHaveStyle({
backgroundColor: 'yellow',
fontWeight: 'bold',
});
spectator.dispatchMouseEvent(spectator.element, 'mouseout');
expect(spectator.element).toHaveStyle({
backgroundColor: 'yellow',
fontWeight: 'normal',
});
});
});
Testing Angular Standalone Components
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { StandaloneComponent } from './standalone.component';
describe('StandaloneComponent', () => {
let spectator: Spectator<StandaloneComponent>;
const createComponent = createComponentFactory({
component: StandaloneComponent,
// No need for imports as they are part of the component itself
});
beforeEach(() => {
spectator = createComponent();
});
it('should create standalone component', () => {
expect(spectator.component).toBeTruthy();
});
});
Testing Deferrable Views (@defer)
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { ComponentWithDefer } from './component-with-defer.component';
describe('ComponentWithDefer', () => {
let spectator: Spectator<ComponentWithDefer>;
const createComponent = createComponentFactory({
component: ComponentWithDefer,
});
beforeEach(() => {
spectator = createComponent();
});
it('should render defer block content', () => {
// Render the completed state of the first defer block
spectator.deferBlock().renderComplete();
expect(spectator.query('.deferred-content')).toExist();
expect(spectator.query('.placeholder-content')).not.toExist();
});
it('should show loading state', () => {
// Render the loading state of the first defer block
spectator.deferBlock().renderLoading();
expect(spectator.query('.loading-indicator')).toExist();
});
it('should show placeholder content', () => {
// Render the placeholder state of the first defer block
spectator.deferBlock().renderPlaceholder();
expect(spectator.query('.placeholder-content')).toExist();
});
it('should work with multiple defer blocks', () => {
// For the second defer block in the template
spectator.deferBlock(1).renderComplete();
expect(spectator.query('.second-deferred-content')).toExist();
});
});
Common Patterns
Query Elements
// By CSS selector
const element = spectator.query('.class-name');
// By directive/component
const child = spectator.query(ChildComponent);
// Multiple elements
const elements = spectator.queryAll('.item');
Trigger Events
// Click events
spectator.click('.button');
spectator.click(buttonElement);
// Input events
spectator.typeInElement('value', 'input');
// Custom events
spectator.triggerEventHandler(MyComponent, 'eventName', eventValue);
Custom Matchers
Spectator provides custom matchers to make assertions more readable:
// DOM matchers
expect('.button').toExist();
expect('.inactive-element').not.toExist();
expect('.title').toHaveText('Welcome');
expect('.username').toContainText('John');
expect('input').toHaveValue('test');
expect('.error').toHaveClass('visible');
expect('button').toBeDisabled();
expect('div').toHaveAttribute('aria-label', 'Close');
expect('.menu').toHaveData({ testId: 'main-menu' });
expect('img').toHaveProperty('src', 'path/to/image.jpg');
expect('.parent').toHaveDescendant('.child');
expect('.parent').toHaveDescendantWithText({
selector: '.child',
text: 'Child text',
});
// Object matchers
expect(object).toBePartial({ id: 1 });
Test Async Operations
it('should handle async operations', async () => {
// Arrange
const response = { data: 'test' };
service.getData.mockResolvedValue(response);
// Act
await spectator.component.loadData();
// Assert
expect(spectator.component.data).toEqual(response);
});
Tips and Tricks
-
Debugging Tests
- Use
spectator.debug()to log the current DOM state - Use
console.logsparingly and remove before committing - Set breakpoints in your IDE for step-by-step debugging
- Use
-
Common Pitfalls
- Don't test implementation details
- Avoid testing third-party libraries
- Don't test multiple concerns in a single test
- Remember to clean up subscriptions
-
Performance
- Mock heavy dependencies
- Keep test setup minimal
- Use
beforeAllfor expensive operations shared across tests
-
Change Detection
- Use
spectator.detectChanges()after modifying component properties - For OnPush components with a host, use
spectator.detectComponentChanges()
- Use
-
Injection
- Use
spectator.inject(Service)to access injected services - Use
spectator.inject(Service, true)to get service from the component injector
- Use
Running Tests
When working in an Nx workspace, there are several ways to run tests:
Running Tests for a Specific Project
# Run all tests for a specific project
npx nx test <project-name>
# Example: Run tests for the core/config library
npx nx test core-config
Running Tests with Watch Mode
# Run tests in watch mode for active development
npx nx test <project-name> --watch
Running Tests with Coverage
# Run tests with coverage reporting
npx nx test <project-name> --code-coverage
Running a Specific Test File
# Run a specific test file
npx nx test <project-name> --test-file=path/to/your.spec.ts
Running Affected Tests
# Run tests only for projects affected by recent changes
npx nx affected:test
These commands help you target exactly which tests to run, making your testing workflow more efficient.
References
- Spectator Documentation - Official documentation for the Spectator testing library
- Jest Documentation - Comprehensive guide to using Jest as a testing framework
- ng-mocks Documentation - Detailed documentation on mocking Angular dependencies effectively
ng-mocks Guide
Overview
ng-mocks is a powerful library that helps with Angular testing by:
- Mocking Components, Directives, Pipes, Modules, Services, and Tokens
- Reducing boilerplate in tests
- Providing a simple interface to access declarations
It's particularly useful for isolating components by mocking their dependencies, which makes tests faster and more reliable.
Global Configuration
For optimal setup, configure ng-mocks in your test setup file:
// src/test-setup.ts or equivalent
import { ngMocks } from 'ng-mocks';
// Auto-spy all methods in mock declarations and providers
ngMocks.autoSpy('jest'); // or 'jasmine'
// Reset customizations after each test automatically
ngMocks.defaultMock(AuthService, () => ({
isLoggedIn$: EMPTY,
currentUser$: EMPTY,
}));
Key APIs
MockBuilder
MockBuilder provides a fluent API to configure TestBed with mocks:
beforeEach(() => {
return MockBuilder(
ComponentUnderTest, // Keep this as real
ParentModule, // Mock everything else
)
.keep(ReactiveFormsModule) // Keep this as real
.mock(SomeOtherDependency, { customConfig: true }); // Custom mock
});
MockRender
MockRender is an enhanced version of TestBed.createComponent that:
- Respects all lifecycle hooks
- Handles OnPush change detection
- Creates a wrapper component that binds inputs and outputs
// Simple rendering
const fixture = MockRender(ComponentUnderTest);
// With inputs
const fixture = MockRender(ComponentUnderTest, {
name: 'Test User',
id: 123,
});
// Access the component instance
const component = fixture.point.componentInstance;
MockInstance
MockInstance helps configure mocks before they're initialized:
// Adding a spy
const saveSpy = MockInstance(StorageService, 'save', jest.fn());
// Verify the spy was called
expect(saveSpy).toHaveBeenCalledWith(expectedData);
ngMocks Helpers
The library provides several helper functions:
// Change form control values
ngMocks.change('[name=email]', 'test@example.com');
// Trigger events
ngMocks.trigger(element, 'click');
ngMocks.trigger(element, 'keyup.control.s'); // Complex events
// Find elements
const emailField = ngMocks.find('[name=email]');
const submitBtn = ngMocks.findAll('button[type="submit"]');
Complete Example
Here's a full example of testing a component with ng-mocks:
describe('ProfileComponent', () => {
// Reset customizations after each test
MockInstance.scope();
beforeEach(() => {
return MockBuilder(ProfileComponent, ProfileModule).keep(
ReactiveFormsModule,
);
});
it('saves profile data on ctrl+s hotkey', () => {
// Prepare test data
const profile = {
email: 'test@email.com',
firstName: 'Test',
lastName: 'User',
};
// Mock service method
const saveSpy = MockInstance(StorageService, 'save', jest.fn());
// Render with inputs
const { point } = MockRender(ProfileComponent, { profile });
// Change form value
ngMocks.change('[name=email]', 'updated@email.com');
// Trigger hotkey
ngMocks.trigger(point, 'keyup.control.s');
// Verify behavior
expect(saveSpy).toHaveBeenCalledWith({
email: 'updated@email.com',
firstName: profile.firstName,
lastName: profile.lastName,
});
});
});
Integration with Spectator
While Spectator and ng-mocks have some overlapping functionality, they can be used together effectively:
describe('CombinedExample', () => {
let spectator: Spectator<MyComponent>;
const createComponent = createComponentFactory({
component: MyComponent,
declarations: [
// Use ng-mocks to mock child components
MockComponent(ComplexChildComponent),
MockDirective(ComplexDirective),
],
providers: [
// Use ng-mocks to mock a service with default behavior
MockProvider(ComplexService),
],
});
beforeEach(() => {
// Configure a mock instance before the component is created
MockInstance(
ComplexService,
'getData',
jest.fn().mockReturnValue(of(['test'])),
);
// Create the component with Spectator
spectator = createComponent();
});
it('should work with mocked dependencies', () => {
// Use Spectator for interactions
spectator.click('button');
// Use ng-mocks to verify interactions with mocked dependencies
const service = ngMocks.get(ComplexService);
expect(service.getData).toHaveBeenCalled();
});
});
When to Use ng-mocks
ng-mocks is particularly useful when:
- You need to mock complex Angular artifacts like components, directives, or modules
- You want to customize mock behavior at the instance level
- You need to simulate complex user interactions
- You're testing parent components that depend on multiple child components
For more details and advanced usage, refer to the official ng-mocks documentation.