37 KiB
Testing Guidelines
Table of Contents
- Introduction
- Core Testing Principles
- Testing Tools
- Best Practices
- End-to-End (E2E) Testing Attributes (
data-what,data-which) - Running Tests (Nx)
- References
Introduction
This document outlines the guidelines and best practices for writing unit tests in this Angular project. Consistent and effective testing ensures code quality, maintainability, and reliability.
Key Requirements:
- Test files must end with
.spec.ts. - Migration to Vitest: New libraries should use Vitest as the primary test runner. Existing libraries continue to use Jest until migrated.
- Testing Framework Migration: New tests should use Angular Testing Utilities (TestBed, ComponentFixture, etc.). Existing tests using Spectator remain until migrated.
- Employ ng-mocks for mocking complex dependencies like child components.
Core Testing Principles
Adhere to these fundamental principles for writing effective unit tests.
Arrange-Act-Assert (AAA) Pattern
Structure your tests using the AAA pattern for clarity and consistency:
- Arrange: Set up the test environment. Initialize objects, mock dependencies, and prepare inputs.
- Act: Execute the code being tested. Call the function or method under scrutiny.
- Assert: Verify the outcome. Check if the actual result matches the expected result.
Isolation: Mocking Dependencies
Unit tests should focus on a single unit of code in isolation.
- Mock External Dependencies: Services, APIs, or other modules the unit interacts with should be mocked to prevent side effects and external failures from impacting the test. Spectator's
mocksarray in factory options simplifies this. - Mock Child Components: When testing a component, mock its child components using Spectator's
overrideComponentsto ensure you are only testing the parent component's logic and template, not the children's implementation details.
Error Case Testing
Go beyond the "happy path". Ensure your tests cover:
- Edge cases
- Invalid inputs
- Error scenarios and how the unit handles them
Clarity and Isolation
- Clear Names: Write descriptive
describeanditblock names that clearly state what is being tested and the expected outcome. - Independent Tests: Ensure tests do not depend on each other. Each test should set up its own state and clean up afterward. Avoid relying on the execution order of tests.
Testing Tools
Jest: The Test Runner
Jest is the testing framework currently used for existing libraries, providing features like test discovery, assertion functions (expect), and mocking capabilities (jest.fn(), jest.spyOn()). Note: New libraries should use Vitest instead of Jest.
Vitest: Modern Testing Framework
Vitest is the modern testing framework being adopted for new libraries. It provides fast execution, native ES modules support, and excellent developer experience.
Vitest Overview
- Fast Execution: Powered by Vite, offering significantly faster test runs than Jest
- Native ES Modules: Built-in support for ES modules without complex configuration
- Jest Compatibility: Compatible with Jest's API, making migration straightforward
- Hot Module Replacement: Tests can run in watch mode with instant feedback
- TypeScript Support: First-class TypeScript support out of the box
- Built-in Coverage: Integrated code coverage reporting with c8
Vitest Configuration
Vitest configuration is typically done in vitest.config.ts or within the Nx project configuration:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import angular from '@analogjs/vite-plugin-angular';
export default defineConfig({
plugins: [angular()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['src/test-setup.ts'],
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test-setup.ts',
],
},
},
});
CI/CD Integration: JUnit and Cobertura Reporting
Both Jest and Vitest are configured to generate JUnit XML reports and Cobertura coverage reports for Azure Pipelines integration.
Jest Configuration (Existing Libraries)
Jest projects inherit JUnit and Cobertura configuration from jest.preset.js:
// jest.preset.js (workspace root)
module.exports = {
...nxPreset,
coverageReporters: ['text', 'cobertura'],
reporters: [
'default',
[
'jest-junit',
{
outputDirectory: 'testresults',
outputName: 'TESTS',
uniqueOutputName: 'true',
classNameTemplate: '{classname}',
titleTemplate: '{title}',
ancestorSeparator: ' › ',
usePathForSuiteName: true,
},
],
],
};
Key Points:
- JUnit XML files are written to
testresults/TESTS-{uuid}.xml - Cobertura coverage reports are written to
coverage/{projectPath}/cobertura-coverage.xml - No additional configuration needed in individual Jest projects
- Run with coverage:
npx nx test <project> --code-coverage
Vitest Configuration (New Libraries)
Vitest projects require explicit JUnit and Cobertura configuration in their vite.config.mts files:
// libs/{domain}/{library}/vite.config.mts
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import angular from '@analogjs/vite-plugin-angular';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default
// @ts-expect-error - Vitest reporter tuple types have complex inference issues, but config works correctly at runtime
defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/{domain}/{library}',
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
setupFiles: ['src/test-setup.ts'],
reporters: [
'default',
['junit', { outputFile: '../../../testresults/junit-{project-name}.xml' }],
],
coverage: {
reportsDirectory: '../../../coverage/libs/{domain}/{library}',
provider: 'v8' as const,
reporter: ['text', 'cobertura'],
},
},
}));
Key Points:
- JUnit Reporter: Built into Vitest, no additional package needed
- Output Path: Adjust relative path based on library depth:
- 3 levels (
libs/domain/library): Use../../../testresults/ - 4 levels (
libs/domain/type/library): Use../../../../testresults/
- 3 levels (
- Coverage Reporter: Add
'cobertura'to the reporter array - TypeScript Suppression: Add
// @ts-expect-errorcomment beforedefineConfigto suppress type inference warnings - Run with Coverage:
npx nx test <project> --coverage.enabled=true
Azure Pipelines Integration
Both Jest and Vitest reports are consumed by Azure Pipelines:
# azure-pipelines.yml
- task: PublishTestResults@2
displayName: Publish Test results
inputs:
testResultsFiles: '**/TESTS-*.xml'
searchFolder: $(Build.StagingDirectory)/testresults
testResultsFormat: JUnit
mergeTestResults: false
failTaskOnFailedTests: true
- task: PublishCodeCoverageResults@2
displayName: Publish code Coverage
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: $(Build.StagingDirectory)/coverage/**/cobertura-coverage.xml
Verification:
- JUnit XML files:
testresults/junit-*.xmlortestresults/TESTS-*.xml - Cobertura XML files:
coverage/libs/{path}/cobertura-coverage.xml
New Library Checklist
When creating a new Vitest-based library, ensure:
- ✅
reportersarray includes both'default'and JUnit configuration - ✅ JUnit
outputFileuses correct relative path depth - ✅ Coverage
reporterarray includes'cobertura' - ✅ Add
// @ts-expect-errorcomment beforedefineConfig()if TypeScript errors appear - ✅ Verify report generation: Run
npx nx test <project> --coverage.enabled=true --skip-cache - ✅ Check files exist:
testresults/junit-{project-name}.xmlcoverage/libs/{path}/cobertura-coverage.xml
Core Testing Features
Vitest provides similar APIs to Jest with enhanced performance:
// Basic test structure
import { describe, it, expect, beforeEach, vi } from 'vitest';
describe('MyService', () => {
beforeEach(() => {
// Setup logic
});
it('should perform expected behavior', () => {
// Test implementation
expect(result).toBe(expected);
});
});
Mocking in Vitest
Vitest provides powerful mocking capabilities:
import { vi } from 'vitest';
// Mock functions
const mockFn = vi.fn();
const mockFnWithReturn = vi.fn().mockReturnValue('mocked value');
// Mock modules
vi.mock('./my-module', () => ({
MyClass: vi.fn(),
myFunction: vi.fn(),
}));
// Spy on existing functions
const spy = vi.spyOn(object, 'method');
Example Test Structures with Vitest
Basic Service Test with Vitest:
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { MyService } from './my-service.service';
import { HttpClient } from '@angular/common/http';
import { of } from 'rxjs';
describe('MyService', () => {
let service: MyService;
let httpClientSpy: any;
beforeEach(() => {
httpClientSpy = {
get: vi.fn(),
post: vi.fn(),
};
TestBed.configureTestingModule({
providers: [
MyService,
{ provide: HttpClient, useValue: httpClientSpy },
],
});
service = TestBed.inject(MyService);
});
it('should fetch data successfully', async () => {
// Arrange
const mockData = { id: 1, name: 'Test' };
httpClientSpy.get.mockReturnValue(of(mockData));
// Act
const result = await service.getData().toPromise();
// Assert
expect(result).toEqual(mockData);
expect(httpClientSpy.get).toHaveBeenCalledWith('/api/data');
});
});
Angular Testing Utilities with Vitest:
For new tests, use Angular's official testing utilities with Vitest:
import { describe, it, expect, beforeEach } from 'vitest';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my-component.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent], // For standalone components
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Angular Testing Utilities: Official Framework
Angular Testing Utilities are the official testing tools provided by the Angular framework. New tests should use these utilities instead of Spectator.
Angular Testing Utilities Overview
- Official Support: Maintained by the Angular team, ensuring compatibility with Angular updates
- Comprehensive API: Full access to Angular's testing capabilities without abstractions
- TestBed Integration: Direct integration with Angular's testing module for dependency injection
- Type Safety: Full TypeScript support with proper type inference
- Standard Approach: Aligns with Angular documentation and community best practices
TestBed Configuration
TestBed is the primary API for configuring and creating Angular testing modules:
import { TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
@Component({
selector: 'app-test',
template: '<h1>{{ title }}</h1>',
standalone: true,
})
class TestComponent {
title = 'Test Title';
}
describe('TestComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TestComponent], // For standalone components
// providers: [], // Add service providers
// declarations: [], // For non-standalone components (legacy)
}).compileComponents();
});
});
Angular Component Testing
Testing components with Angular Testing Utilities:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { MyComponent } from './my-component.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent],
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});
it('should display title', () => {
// Arrange
const expectedTitle = 'Test Title';
component.title = expectedTitle;
// Act
fixture.detectChanges();
// Assert
const titleElement: HTMLElement = fixture.nativeElement.querySelector('h1');
expect(titleElement.textContent).toContain(expectedTitle);
});
it('should emit event on button click', () => {
// Arrange
spyOn(component.buttonClicked, 'emit');
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
// Act
button.click();
// Assert
expect(component.buttonClicked.emit).toHaveBeenCalled();
});
it('should find element by directive', () => {
// Using DebugElement for more advanced querying
const debugElement: DebugElement = fixture.debugElement;
const buttonDebugElement = debugElement.query(By.css('button'));
expect(buttonDebugElement).toBeTruthy();
});
});
Angular Service Testing
Testing services with dependency injection:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
import { User } from './user.model';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService],
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Verify no outstanding HTTP requests
});
it('should fetch users', () => {
// Arrange
const mockUsers: User[] = [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
];
// Act
service.getUsers().subscribe(users => {
// Assert
expect(users).toEqual(mockUsers);
});
// Assert HTTP request
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});
Angular HTTP Testing
Using HttpClientTestingModule for HTTP testing:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ApiService } from './api.service';
describe('ApiService', () => {
let service: ApiService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [ApiService],
});
service = TestBed.inject(ApiService);
httpMock = TestBed.inject(HttpTestingController);
});
it('should handle HTTP errors', () => {
const errorMessage = 'Server error';
service.getData().subscribe({
next: () => fail('Should have failed'),
error: (error) => {
expect(error.status).toBe(500);
expect(error.statusText).toBe('Server Error');
},
});
const req = httpMock.expectOne('/api/data');
req.flush(errorMessage, { status: 500, statusText: 'Server Error' });
});
});
Mocking with Angular
Angular provides several ways to mock dependencies:
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
// Mock service implementation
const mockUserService = {
getUsers: jasmine.createSpy('getUsers').and.returnValue(of([])),
createUser: jasmine.createSpy('createUser').and.returnValue(of({})),
};
describe('ComponentWithDependency', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ComponentWithDependency],
providers: [
{ provide: UserService, useValue: mockUserService },
],
}).compileComponents();
});
it('should call user service', () => {
const fixture = TestBed.createComponent(ComponentWithDependency);
const component = fixture.componentInstance;
component.loadUsers();
expect(mockUserService.getUsers).toHaveBeenCalled();
});
});
Spectator: Simplifying Angular Tests (Legacy)
Spectator significantly reduces boilerplate and simplifies testing Angular components, directives, and services. Note: This is legacy for existing tests. New tests should use Angular Testing Utilities.
Spectator Overview
- Less Boilerplate: Streamlines
TestBedconfiguration. - Easy DOM Querying: Provides intuitive methods (
query,queryAll,byText, etc.) to find elements. - Clean Event API: Simplifies triggering user interactions (
click,typeInElement,keyboard). - Custom Matchers: Offers Jest matchers (
toExist,toHaveText,toBeDisabled, etc.) for more readable assertions.
Core Factory Methods
Spectator provides factory functions to set up the testing environment:
createComponentFactory: For testing components in isolation.createHostFactory: For testing components within a host template (useful for inputs/outputs).createDirectiveFactory: For testing directives.createServiceFactory: For testing services.createHttpFactory: For testing services that interact withHttpClient.
Example Usage (Component):
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { MyComponent } from './my-component.component';
import { SomeService } from '../some.service';
const createComponent = createComponentFactory({
component: MyComponent,
mocks: [SomeService], // Automatically mocks SomeService
detectChanges: false, // Control initial change detection
});
let spectator: Spectator<MyComponent>;
beforeEach(() => spectator = createComponent());
Querying Elements
Use Spectator's query methods to find elements for interaction or assertion:
// CSS selectors
const button = spectator.query('button.submit');
const inputs = spectator.queryAll('input');
// By component/directive type
const child = spectator.query(ChildComponent);
// By text, label, placeholder, etc.
const submitBtn = spectator.query(byText('Submit'));
const usernameInput = spectator.query(byLabel('Username'));
Working with Inputs/Outputs
Easily set inputs and subscribe to outputs:
// Set single input
spectator.setInput('user', { name: 'Test' });
// Set multiple inputs
spectator.setInput({ user: { name: 'Test' }, isAdmin: true });
// Host component input
// spectator.setHostInput('propName', value); // If using createHostFactory
// Subscribe to output
const outputSpy = jest.fn();
spectator.output('userLoggedIn').subscribe(outputSpy);
spectator.click('button.login');
expect(outputSpy).toHaveBeenCalled();
Event Triggering
Simulate user interactions:
spectator.click('.login-button');
spectator.typeInElement('test user', 'input[name="username"]');
spectator.keyboard.pressEnter('input[name="password"]');
spectator.selectOption(spectator.query('select'), 'Option 2');
Custom Matchers
Improve assertion readability:
expect('.error-message').not.toExist();
expect('h1.title').toHaveText('Welcome');
expect('input.email').toHaveValue('test@example.com');
expect('button.submit').toBeDisabled();
expect('.parent').toHaveDescendant('.child');
ng-mocks: Advanced Mocking
ng-mocks excels at mocking complex Angular artifacts like Components, Directives, Pipes, and Modules, further enhancing test isolation. Note: While ng-mocks offers powerful features like MockComponent and MockDirective, in a standalone component architecture, the primary way to replace dependencies (including child components or directives) is often through Spectator's overrideComponents feature, as shown in the examples below. ng-mocks might still be useful for features like MockProvider or MockInstance.
ng-mocks Overview
- Deep Mocking: Easily mocks entire modules or specific declarations/providers.
- Boilerplate Reduction: Simplifies complex
TestBedsetups. - Helper Utilities: Provides functions for interactions (
ngMocks.change,ngMocks.click) and querying (ngMocks.find).
Key APIs
MockBuilder: Fluent API to configureTestBed, specifying what to keep real and what to mock.MockRender: EnhancedTestBed.createComponentthat respects lifecycle hooks and simplifies input/output binding.MockComponent,MockDirective,MockPipe,MockProvider: Functions to create shallow mocks of specific artifacts. (Note: For standalone, preferoverrideComponentsfor components/directives).MockInstance: Allows customizing mock behavior before initialization (e.g., setting up spies).
When to Use ng-mocks
- Mocking specific services or pipes using
MockProviderwhen more control is needed than Spectator'smocksarray offers. - When needing fine-grained control over mock behavior using
MockInstance. - Testing components that rely heavily on content projection (
ng-content). - Caution: Avoid
MockComponentandMockDirectivefor standalone components/directives; use Spectator'soverrideComponentsinstead.
Integration with Angular Testing Utilities
ng-mocks integrates well with Angular Testing Utilities. Use Angular's TestBed for the primary test setup and ng-mocks for advanced mocking scenarios:
import { TestBed } from '@angular/core/testing';
import { MockBuilder, MockRender, MockProvider } from 'ng-mocks';
import { ParentComponent } from './parent.component';
import { ChildService } from './child.service';
describe('ParentComponent with ng-mocks', () => {
beforeEach(() => {
return MockBuilder(ParentComponent)
.provide(MockProvider(ChildService, {
getData: () => of(['mocked data']),
}));
});
it('should render with mocked service', () => {
const fixture = MockRender(ParentComponent);
const component = fixture.point.componentInstance;
expect(component).toBeTruthy();
});
});
Integration with Spectator (Legacy)
Spectator and ng-mocks can work together for existing tests. Use Spectator for the primary test setup and interaction/assertion. Use Spectator's overrideComponents to replace standalone child components/directives. Use ng-mocks' MockProvider or MockInstance when needed for services or advanced mocking scenarios.
// Example using Spectator's overrideComponents for a standalone child
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { ParentComponent } from './parent.component';
import { ChildComponent } from './child.component';
import { MockChildComponent } from './mock-child.component'; // A standalone mock component
import { ComplexService } from './complex.service';
import { MockProvider } from 'ng-mocks'; // ng-mocks can still be used for providers
const createComponent = createComponentFactory({
component: ParentComponent, // Assume ParentComponent imports ChildComponent
overrideComponents: [
[
ParentComponent, // Target the component whose imports need overriding
{
remove: { imports: [ChildComponent] }, // Remove the real standalone component
add: { imports: [MockChildComponent] }, // Add the mock standalone component
},
],
],
providers: [
MockProvider(ComplexService, { // Mock service using ng-mocks' MockProvider
getData: () => of(['mocked data']),
}),
],
});
// ... rest of the test using Spectator API ...
Best Practices
General
- Focus: Each test (
itblock) should ideally test one specific behavior or scenario. - Readability: Use clear variable names and structure tests logically (AAA).
- Avoid Logic: Do not put complex logic within tests. Keep them simple and direct.
- No Implementation Details: Test the public API or observable behavior, not private methods or internal state.
Component Testing
- Use
createComponentFactoryfor most components. UsecreateHostFactoryonly when testing interaction via template bindings (inputs/outputs) is necessary. - Test inputs change behavior as expected.
- Test outputs are emitted correctly upon interaction.
- Test conditional rendering (
@if,@switch) and loops (@for). - Verify important DOM elements are rendered correctly based on state.
- Mock Standalone Child Components/Directives using Spectator's
overrideComponentsfeature to isolate the component under test.
Service Testing
- Use
createServiceFactory. - Mock dependencies using the
mocksarray. - Test public methods, ensuring they return expected values or have the correct side effects (like calling mocked dependencies).
- For services using
HttpClient, usecreateHttpFactoryandSpectatorHttpfor easy request expectation (spectator.expectOne).
Directive Testing
- Use
createDirectiveFactory. - Test how the directive affects the host element or other elements based on inputs or events.
- If a directive has standalone dependencies, use
overrideDirectives(similar tooverrideComponents) in the factory options.
Mocking
- Prefer Spectator's
mocksarray for simple service mocking. - Use
spectator.inject(ServiceToMock)to get the mocked instance and configure its methods (e.g.,mockService.method.mockReturnValue(...)). - Use Spectator's
overrideComponentsoroverrideDirectivesto replace standalone component/directive dependencies with mocks. - Use
MockProviderfromng-mocksif you need more control over service mocking than Spectator'smocksarray provides, or for mocking tokens/values.
Async Operations
- Use
async/awaitwith Promises. - For RxJS Observables, subscribe within the test or use helpers like
waitForAsyncor Jest's timer mocks if needed, though often direct subscription is sufficient. Spectator handles basic async operations well.
Performance
- Mock heavy dependencies (e.g., HTTP calls, complex computations).
- Keep test setup (
beforeEach) lean. UsebeforeAllonly for setup that is truly read-only and shared across all tests in adescribeblock.
Debugging
- Use
spectator.debug()to print the current component's DOM structure to the console. - Use standard
console.logstatements temporarily. - Utilize your IDE's debugger with breakpoints.
End-to-End (E2E) Testing Attributes (data-what, data-which)
In end-to-end (E2E) testing of web applications, data-what and data-which are custom HTML data attributes used to enhance testability. They help identify elements or components in the DOM for testing purposes, making it easier for testing frameworks (like Selenium, Cypress, or WebdriverIO) to locate and interact with specific UI elements.
Purpose of data-what and data-which
- Identification: These attributes provide a stable, test-specific way to tag elements, avoiding reliance on brittle selectors like CSS classes, IDs, or XPath, which may change due to styling or structural updates.
- Semantics: They can describe the purpose (
data-what) or specific instance/context (data-which) of an element, improving clarity in test scripts. - Maintainability: By using custom attributes, developers can decouple test logic from presentation, reducing the risk of tests breaking when the UI is refactored.
Example Usage
<button data-what="submit-button" data-which="form-login">Login</button>
data-what="submit-button": Indicates the element's role or type (e.g., a submit button).data-which="form-login": Specifies the context or instance (e.g., the submit button for the login form).
In a test script (e.g., using WebdriverIO):
// Find the specific button
const loginButton = await $('[data-what="submit-button"][data-which="form-login"]');
await loginButton.click();
This targets the exact button, even if its class or ID changes.
Why Use Them?
- Robustness: Unlike classes or IDs, which are often tied to styling or JavaScript,
data-whatanddata-whichare dedicated to testing, reducing the chance of accidental changes breaking tests. - Clarity: They make test scripts self-documenting by describing the element's purpose and context.
- Scalability: In complex applications with multiple similar elements (e.g., multiple "Save" buttons),
data-whichhelps differentiate them.
Best Practices
-
Use clear, consistent naming conventions (e.g.,
data-what="button",data-which="checkout-page"). -
Avoid overusing or duplicating attributes unnecessarily.
-
Use additional
data-*attributes for dynamic identifiers: When you need to associate specific data like an item ID or name with an element for testing, add separatedata-*attributes instead of encoding them intodata-which. This keeps selectors clean and allows for more precise targeting.Example:
<oms-feature-return-summary-item [returnProcess]="item" data-what="list-item" data-which="return-process-item" [attr.data-item-id]="item.id" [attr.data-item-name]="item.name" ></oms-feature-return-summary-item>E2E Test Query (WebdriverIO):
// Find a specific item by ID const specificItem = await $('[data-what="list-item"][data-which="return-process-item"][data-item-id="12345"]'); await specificItem.waitForExist(); // Find all return process items const allItems = await $$('[data-what="list-item"][data-which="return-process-item"]'); console.log(`Found ${allItems.length} items.`); -
Ensure developers and testers agree on the attribute schema to maintain consistency.
-
Verify in Unit Tests: Ensure these
data-*attributes are correctly applied in your component unit tests. Use Spectator's querying capabilities to check for their presence and dynamic values.Unit Test Snippet (Spectator):
it('should apply correct data attributes', () => { // Arrange const testItem = { id: 'abc', name: 'Test Item' }; spectator.setInput('returnProcess', testItem); spectator.detectChanges(); // Act const element = spectator.queryHost('[data-what="list-item"]'); // Or query the specific element inside the component // Assert expect(element).toHaveAttribute('data-what', 'list-item'); expect(element).toHaveAttribute('data-which', 'return-process-item'); expect(element).toHaveAttribute('data-item-id', 'abc'); expect(element).toHaveAttribute('data-item-name', 'Test Item'); });
While data-what and data-which are not standardized HTML attributes, their use as custom data-* attributes aligns with HTML5 specifications, making them valid and widely supported. If you encounter them in a specific framework or codebase, check the project's testing documentation for their exact conventions.
Running Tests (Nx)
Use the Nx CLI to run tests efficiently within the monorepo. The commands differ depending on whether the project uses Jest or Vitest.
Run Tests for a Specific Project
For Jest-based projects (existing libraries):
npx nx test <project-name>
# Example: npx nx test core-logging
# Example: npx nx test isa-app
For Vitest-based projects (new libraries):
npx nx test <project-name>
# Nx automatically detects and uses the appropriate test runner
# Example: npx nx test new-feature-lib
Run Tests in Watch Mode
Jest projects:
npx nx test <project-name> --watch
Vitest projects:
npx nx test <project-name> --watch
# Or use Vitest's native watch mode
npx vitest --watch
Run Tests with Coverage Report
Jest projects:
npx nx test <project-name> --code-coverage
Vitest projects:
npx nx test <project-name> --coverage
# Or use Vitest directly
npx vitest --coverage
(Coverage reports are typically generated in the coverage/ directory)
Run a Specific Test File
Jest projects:
npx nx test <project-name> --test-file=libs/core/utils/src/lib/my-util.spec.ts
Vitest projects:
npx nx test <project-name> --test-file=libs/new-feature/src/lib/my-util.spec.ts
# Or use Vitest directly
npx vitest src/lib/my-util.spec.ts
Run Tests with UI (Vitest only)
Vitest provides an interactive UI for test exploration:
npx vitest --ui
Run Affected Tests
Optimize CI/CD by running tests only for projects affected by your code changes:
# Run tests for projects affected by changes compared to main branch
npx nx affected:test --base=main --head=HEAD
# Or run tests for uncommitted changes
npx nx affected:test
Migration Notes
Test Runner Migration
- Existing libraries continue using Jest until explicitly migrated
- New libraries should be configured with Vitest from the start
- Both test runners can coexist in the same monorepo
- Nx handles the appropriate test runner selection automatically based on project configuration
Testing Framework Migration
- New tests should use Angular Testing Utilities (TestBed, ComponentFixture, etc.)
- Existing Spectator tests remain until explicitly migrated
- Both testing approaches can coexist in the same codebase
- Angular Testing Utilities provide better long-term maintainability and align with Angular's official documentation
Benefits of the New Approach
- Official Support: Angular Testing Utilities are maintained by the Angular team
- Better Integration: Direct integration with Angular's dependency injection and testing modules
- Improved Performance: Vitest provides significantly faster test execution than Jest
- Future-Proof: Aligns with Angular's testing direction and community best practices
References
- Jest Documentation: Official Jest documentation.
- Vitest Documentation: Official Vitest documentation.
- Spectator Documentation: Official Spectator documentation.
- ng-mocks Documentation: Official ng-mocks documentation.
- Angular Testing Guide: Official Angular testing concepts.