# Testing Guidelines **Table of Contents** 1. [Introduction](#introduction) 2. [Core Testing Principles](#core-testing-principles) * [Arrange-Act-Assert (AAA) Pattern](#arrange-act-assert-aaa-pattern) * [Isolation: Mocking Dependencies](#isolation-mocking-dependencies) * [Error Case Testing](#error-case-testing) * [Clarity and Isolation](#clarity-and-isolation) 3. [Testing Tools](#testing-tools) * [Jest: The Test Runner](#jest-the-test-runner) * [Vitest: Modern Testing Framework](#vitest-modern-testing-framework) * [Overview](#vitest-overview) * [Configuration](#vitest-configuration) * [CI/CD Integration: JUnit and Cobertura Reporting](#cicd-integration-junit-and-cobertura-reporting) * [Core Testing Features](#core-testing-features) * [Mocking in Vitest](#mocking-in-vitest) * [Example Test Structures with Vitest](#example-test-structures-with-vitest) * [Angular Testing Utilities: Official Framework](#angular-testing-utilities-official-framework) * [Overview](#angular-testing-utilities-overview) * [TestBed Configuration](#testbed-configuration) * [Component Testing](#angular-component-testing) * [Service Testing](#angular-service-testing) * [HTTP Testing](#angular-http-testing) * [Mocking with Angular](#mocking-with-angular) * [Spectator: Simplifying Angular Tests (Legacy)](#spectator-simplifying-angular-tests-legacy) * [Overview](#spectator-overview) * [Core Factory Methods](#core-factory-methods) * [Querying Elements](#querying-elements) * [Working with Inputs/Outputs](#working-with-inputsoutputs) * [Event Triggering](#event-triggering) * [Custom Matchers](#custom-matchers) * [ng-mocks: Advanced Mocking](#ng-mocks-advanced-mocking) * [Overview](#ng-mocks-overview) * [Key APIs](#key-apis) * [When to Use ng-mocks](#when-to-use-ng-mocks) * [Integration with Angular Testing Utilities](#integration-with-angular-testing-utilities) 4. [Best Practices](#best-practices) * [General](#general) * [Component Testing](#component-testing) * [Service Testing](#service-testing) * [Directive Testing](#directive-testing) * [Mocking](#mocking) * [Async Operations](#async-operations) * [Performance](#performance) * [Debugging](#debugging) 5. [End-to-End (E2E) Testing Attributes (`data-what`, `data-which`)](#end-to-end-e2e-testing-attributes-data-what-data-which) 6. [Running Tests (Nx)](#running-tests-nx) 7. [References](#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: 1. **Arrange**: Set up the test environment. Initialize objects, mock dependencies, and prepare inputs. 2. **Act**: Execute the code being tested. Call the function or method under scrutiny. 3. **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 `mocks` array in factory options simplifies this. - **Mock Child Components**: When testing a component, mock its child components using Spectator's `overrideComponents` to 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 `describe` and `it` block 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](https://jestjs.io/docs/getting-started) 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](https://vitest.dev/) 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: ```typescript // 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`: ```javascript // 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 --code-coverage` ##### Vitest Configuration (New Libraries) Vitest projects require explicit JUnit and Cobertura configuration in their `vite.config.mts` files: ```typescript // libs/{domain}/{library}/vite.config.mts /// 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/` - **Coverage Reporter**: Add `'cobertura'` to the reporter array - **TypeScript Suppression**: Add `// @ts-expect-error` comment before `defineConfig` to suppress type inference warnings - **Run with Coverage**: `npx nx test --coverage.enabled=true` ##### Azure Pipelines Integration Both Jest and Vitest reports are consumed by Azure Pipelines: ```yaml # 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-*.xml` or `testresults/TESTS-*.xml` - Cobertura XML files: `coverage/libs/{path}/cobertura-coverage.xml` ##### New Library Checklist When creating a new Vitest-based library, ensure: 1. ✅ `reporters` array includes both `'default'` and JUnit configuration 2. ✅ JUnit `outputFile` uses correct relative path depth 3. ✅ Coverage `reporter` array includes `'cobertura'` 4. ✅ Add `// @ts-expect-error` comment before `defineConfig()` if TypeScript errors appear 5. ✅ Verify report generation: Run `npx nx test --coverage.enabled=true --skip-cache` 6. ✅ Check files exist: - `testresults/junit-{project-name}.xml` - `coverage/libs/{path}/cobertura-coverage.xml` #### Core Testing Features Vitest provides similar APIs to Jest with enhanced performance: ```typescript // 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: ```typescript 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:** ```typescript 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: ```typescript 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; 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](https://angular.dev/guide/testing) 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: ```typescript import { TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; @Component({ selector: 'app-test', template: '

{{ title }}

', 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: ```typescript 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; 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: ```typescript 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: ```typescript 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: ```typescript 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](https://ngneat.github.io/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 `TestBed` configuration. - **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 with `HttpClient`. *Example Usage (Component):* ```typescript 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; beforeEach(() => spectator = createComponent()); ``` #### Querying Elements Use Spectator's query methods to find elements for interaction or assertion: ```typescript // 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: ```typescript // 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: ```typescript 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: ```typescript 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](https://ng-mocks.sudo.eu/) 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 `TestBed` setups. - **Helper Utilities**: Provides functions for interactions (`ngMocks.change`, `ngMocks.click`) and querying (`ngMocks.find`). #### Key APIs - **`MockBuilder`**: Fluent API to configure `TestBed`, specifying what to keep real and what to mock. - **`MockRender`**: Enhanced `TestBed.createComponent` that respects lifecycle hooks and simplifies input/output binding. - **`MockComponent`, `MockDirective`, `MockPipe`, `MockProvider`**: Functions to create shallow mocks of specific artifacts. (**Note:** For standalone, prefer `overrideComponents` for 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 `MockProvider` when more control is needed than Spectator's `mocks` array offers. - When needing fine-grained control over mock behavior using `MockInstance`. - Testing components that rely heavily on content projection (`ng-content`). - **Caution:** Avoid `MockComponent` and `MockDirective` for standalone components/directives; use Spectator's `overrideComponents` instead. #### 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: ```typescript 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. ```typescript // 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 (`it` block) 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 `createComponentFactory` for most components. Use `createHostFactory` only 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 `overrideComponents` feature to isolate the component under test. ### Service Testing - Use `createServiceFactory`. - Mock dependencies using the `mocks` array. - Test public methods, ensuring they return expected values or have the correct side effects (like calling mocked dependencies). - For services using `HttpClient`, use `createHttpFactory` and `SpectatorHttp` for 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 to `overrideComponents`) in the factory options. ### Mocking - Prefer Spectator's `mocks` array 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 `overrideComponents` or `overrideDirectives` to replace standalone component/directive dependencies with mocks. - Use `MockProvider` from `ng-mocks` if you need more control over service mocking than Spectator's `mocks` array provides, or for mocking tokens/values. ### Async Operations - Use `async/await` with Promises. - For RxJS Observables, subscribe within the test or use helpers like `waitForAsync` or 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. Use `beforeAll` only for setup that is truly read-only and shared across all tests in a `describe` block. ### Debugging - Use `spectator.debug()` to print the current component's DOM structure to the console. - Use standard `console.log` statements 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 ```html ``` - `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): ```javascript // 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-what` and `data-which` are 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-which` helps 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 separate `data-*` attributes instead of encoding them into `data-which`. This keeps selectors clean and allows for more precise targeting. *Example:* ````html ```` *E2E Test Query (WebdriverIO):* ````javascript // 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):* ````typescript 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):** ```bash npx nx test # Example: npx nx test core-logging # Example: npx nx test isa-app ``` **For Vitest-based projects (new libraries):** ```bash npx nx test # Nx automatically detects and uses the appropriate test runner # Example: npx nx test new-feature-lib ``` ### Run Tests in Watch Mode **Jest projects:** ```bash npx nx test --watch ``` **Vitest projects:** ```bash npx nx test --watch # Or use Vitest's native watch mode npx vitest --watch ``` ### Run Tests with Coverage Report **Jest projects:** ```bash npx nx test --code-coverage ``` **Vitest projects:** ```bash npx nx test --coverage # Or use Vitest directly npx vitest --coverage ``` *(Coverage reports are typically generated in the `coverage/` directory)* ### Run a Specific Test File **Jest projects:** ```bash npx nx test --test-file=libs/core/utils/src/lib/my-util.spec.ts ``` **Vitest projects:** ```bash npx nx test --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: ```bash npx vitest --ui ``` ### Run Affected Tests Optimize CI/CD by running tests only for projects affected by your code changes: ```bash # 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](https://jestjs.io/docs/getting-started)**: Official Jest documentation. - **[Vitest Documentation](https://vitest.dev/)**: Official Vitest documentation. - **[Spectator Documentation](https://ngneat.github.io/spectator/)**: Official Spectator documentation. - **[ng-mocks Documentation](https://ng-mocks.sudo.eu/)**: Official ng-mocks documentation. - **[Angular Testing Guide](https://angular.dev/guide/testing)**: Official Angular testing concepts.