JSPM

@halverscheid-fiae.de/angular-testing-factory

1.5.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 11
  • Score
    100M100P100Q60767F
  • License MIT

Type-safe Angular service mocking with zero drift guarantee for Angular 20+

Package Exports

    This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@halverscheid-fiae.de/angular-testing-factory) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

    Readme

    @halverscheid-fiae.de/angular-testing-factory

    Revolutionary type-safe Angular service mocking for Angular 20+
    Zero Mock Driftβ„’ guarantee with compile-time validation

    npm version npm downloads license bundle size TypeScript Angular Jest

    🎯 Why This Library?

    The Problem: Angular testing is painful. Manual mocks break when services change, TypeScript can't catch mock drift, and every new service needs tons of boilerplate.

    The Solution: This library provides compile-time safe mocking with zero configuration for 90% of use cases, and zero mock drift for your custom services.

    ✨ Revolutionary Features

    • 🎯 Zero Mock Driftβ„’: TypeScript satisfies catches mock inconsistencies at compile-time
    • ⚑ One-Line Providers: provideHttpClientMock() - Done!
    • πŸš€ Automated CI/CD: Semantic versioning with automatic NPM publishing
    • πŸ§ͺ 100% Test Coverage: All 88 tests pass with comprehensive coverage
    • 🎯 Squash & Merge: PR-based workflow with semantic commit messages
    • πŸ”„ Override Anything: Per-test customization with the Factory Pattern
    • πŸ›‘οΈ 100% Type Safe: Full IntelliSense and compile-time validation
    • πŸ“¦ Angular 20+ Native: Signals, Standalone Components, modern inject()
    • οΏ½ Zero Config: Works out-of-the-box with sensible defaults

    πŸš€ Quick Start

    Installation

    npm install --save-dev @halverscheid-fiae.de/angular-testing-factory

    90% Use Case: Preset Collections

    import { TestBed } from '@angular/core/testing';
    import { 
      provideHttpClientMock, 
      provideRouterMock, 
      provideMatDialogMock 
    } from '@halverscheid-fiae.de/angular-testing-factory';
    
    describe('MyComponent', () => {
      beforeEach(() => {
        TestBed.configureTestingModule({
          providers: [
            provideHttpClientMock(),    // ← HttpClient with sensible defaults
            provideRouterMock(),        // ← Router with navigation mocks
            provideMatDialogMock()      // ← MatDialog with dialog mocks
          ]
        });
      });
    
      it('should work perfectly', () => {
        // Your component gets fully mocked dependencies!
      });
    });

    πŸ”₯ The Revolutionary Factory Pattern

    Create Once, Override Everywhere

    // 1. Create your factory ONCE (in test-setup.ts or similar)
    const provideMyBusinessServiceMock = createServiceProviderFactory(MyBusinessService, {
      calculateRevenue: jest.fn(() => of(1000)),
      processPayment: jest.fn(() => Promise.resolve(false)),
      currentUser: signal({ id: 1, name: 'Test User' }),
      isLoading: signal(false)
    });
    
    // 2. Use everywhere with per-test overrides:
    describe('Revenue Tests', () => {
      it('should handle high revenue', () => {
        TestBed.configureTestingModule({
          providers: [
            provideMyBusinessServiceMock({ 
              calculateRevenue: jest.fn(() => of(50000)) // ← Override just this!
            })
          ]
        });
        // Test high revenue scenario
      });
    
      it('should handle payment failures', () => {
        TestBed.configureTestingModule({
          providers: [
            provideMyBusinessServiceMock({ 
              processPayment: jest.fn(() => Promise.reject('Card declined'))
            })
          ]
        });
        // Test payment failure scenario
      });
    
      it('should use sensible defaults', () => {
        TestBed.configureTestingModule({
          providers: [
            provideMyBusinessServiceMock() // ← All defaults, no overrides
          ]
        });
        // Test normal flow
      });
    });

    πŸš€ Quick Migration Guide

    From Manual Window Mocking

    // ❌ Before: Manual & Error-prone
    beforeEach(() => {
      (global as any).window = {
        innerWidth: 1024,
        addEventListener: jest.fn(),
        // Missing tons of properties...
      };
    });
    
    // βœ… After: Complete & Type-safe
    beforeEach(() => {
      const { providers, cleanup } = provideCompleteWindowMock({
        overrides: { innerWidth: 1024 },
        mockGlobal: true
      });
      
      TestBed.configureTestingModule({ providers });
      cleanup = windowCleanup;
    });

    From Direct Injection Errors

    // ❌ Before: Runtime Injection Errors
    TestBed.inject(Window); // NG0201 Error!
    TestBed.inject(Document); // NG0201 Error!
    
    // βœ… After: Proper Token Usage
    import { WINDOW_TOKEN, DOCUMENT_TOKEN } from '@halverscheid-fiae.de/angular-testing-factory';
    
    TestBed.inject(WINDOW_TOKEN); // βœ… Works!
    TestBed.inject(DOCUMENT_TOKEN); // βœ… Works!

    ✨ New: Angular Core Extensions

    // πŸ†• Complete test setup in one line
    TestBed.configureTestingModule({
      providers: provideAngularCoreMocks({
        activatedRoute: {
          snapshot: { params: { id: '123' } }
        },
        window: {
          innerWidth: 1920,
          localStorage: mockStorage()
        }
      })
    });
    
    // πŸ†• Individual providers for specific needs
    TestBed.configureTestingModule({
      providers: [
        provideActivatedRouteMock({
          params: of({ productId: '456' }),
          queryParams: of({ tab: 'details' })
        }),
        provideFormBuilderMock(),
        provideElementRefMock<HTMLInputElement>({
          nativeElement: mockInputElement
        })
      ]
    });

    The Magic: Zero Mock Driftβ„’

    interface MyBusinessService {
      calculateRevenue(): Observable<number>;
      processPayment(amount: number): Promise<boolean>;
      currentUser: Signal<User>;
      isLoading: WritableSignal<boolean>;
    }
    
    // βœ… This will catch ANY drift at compile-time:
    const provideMyBusinessServiceMock = createServiceProviderFactory(MyBusinessService, {
      calculateRevenue: jest.fn(() => of(1000)),
      // ❌ If you forget a method β†’ TypeScript error!
      // ❌ If you add wrong method β†’ TypeScript error!  
      // ❌ If return type changes β†’ TypeScript error!
      // ❌ If service interface changes β†’ TypeScript error!
    });
    import { of } from 'rxjs';
    import { provideHttpClientMock } from '@halverscheid-fiae.de/angular-testing-factory';
    
    // Mock HTTP calls with specific responses
    TestBed.configureTestingModule({
      providers: [
        provideHttpClientMock({
          get: jest.fn(() => of({ data: 'custom response' })),
          post: jest.fn(() => of({ success: true }))
        })
      ]
    });

    πŸ“– API Reference

    Core Functions

    • createMockProvider<T>(token, mockService) - Creates Angular Provider for mocks
    • createMockService<T>(defaults, overrides) - Creates type-safe mock objects

    Preset Providers

    Angular Common

    • provideHttpClientMock(overrides?) - HttpClient Mock
    • provideRouterMock(overrides?) - Router Mock
    • provideLocationMock(overrides?) - Location Mock
    • provideAngularCommonMocks() - All Common Services

    Angular Core Extensions πŸ†•

    • provideActivatedRouteMock(overrides?) - ActivatedRoute Mock (Params, QueryParams, Data)
    • provideFormBuilderMock(overrides?) - FormBuilder Mock (Reactive Forms)
    • provideDomSanitizerMock(overrides?) - DomSanitizer Mock (Security Bypass)

    Browser API Mocks πŸ†•

    • provideElementRefMock<T>(overrides?) - ElementRef Mock with Generic Support
    • provideDocumentMock(overrides?) - Document Mock (DOM Operations)
    • provideWindowMock(overrides?) - Window Mock for Token-based Injection
    • setupGlobalWindowMock(overrides?) - Global Window Mock for Direct Access πŸ”₯
    • provideCompleteWindowMock(options?) - Combined Token + Global Window Mock πŸ”₯

    🌟 Advanced Window Mocking

    Problem Solved: Components using window directly vs. WINDOW_TOKEN injection

    // ❌ Traditional approach: Only works for token-based injection
    providers: [provideWindowMock({ innerWidth: 800 })]
    
    // βœ… New approach: Covers both use cases
    const { providers, cleanup } = provideCompleteWindowMock({
      overrides: { innerWidth: 800 },
      mockGlobal: true  // Also mocks global window object
    });
    
    TestBed.configureTestingModule({ providers });
    // cleanup() restores original window after tests

    Common Use Cases & Solutions:

    ❌ Problem: ɡNotFound: NG0201: No provider found for Window

    // Wrong - Window is not an Angular token
    windowMock = TestBed.inject(Window);

    βœ… Solution 1: Use WINDOW_TOKEN for token-based injection

    import { WINDOW_TOKEN, provideWindowMock } from '@halverscheid-fiae.de/angular-testing-factory';
    
    beforeEach(() => {
      TestBed.configureTestingModule({
        providers: [provideWindowMock({ innerWidth: 1200 })]
      });
      
      windowMock = TestBed.inject(WINDOW_TOKEN); // βœ… Correct token
    });

    βœ… Solution 2: Complete Window mocking (recommended)

    import { provideCompleteWindowMock, WINDOW_TOKEN } from '@halverscheid-fiae.de/angular-testing-factory';
    
    describe('MyComponent', () => {
      let cleanup: (() => void) | undefined;
    
      beforeEach(() => {
        const result = provideCompleteWindowMock({
          overrides: { innerWidth: 1200, location: { href: 'http://test.com' } },
          mockGlobal: true // Mocks both token and global access
        });
        
        cleanup = result.cleanup;
    
        TestBed.configureTestingModule({
          providers: result.providers
        });
        
        // Both work now:
        windowMock = TestBed.inject(WINDOW_TOKEN); // Token-based
        // window.innerWidth also works in component code
      });
    
      afterEach(() => {
        cleanup?.(); // Clean up global window mock
      });
    });

    βœ… Solution 3: Component injection pattern

    // In your component - use token-based injection:
    import { inject } from '@angular/core';
    import { WINDOW_TOKEN } from '@halverscheid-fiae.de/angular-testing-factory';
    
    @Component({...})
    export class MyComponent {
      private window = inject(WINDOW_TOKEN);
      
      onResize() {
        const width = this.window.innerWidth; // βœ… Testable
      }
    }

    Use Cases:

    • Token-based: inject(WINDOW_TOKEN) in Angular services
    • Direct access: window.innerWidth in legacy components
    • Global mocking: Testing code that accesses window directly

    Convenience Bundles πŸ†•

    • provideAngularCoreMocks(overrides?) - All Critical Angular Core Services
    • provideAngularCommonMocks() - Legacy Common Services Bundle

    Angular Material

    • provideMatDialogMock(overrides?) - MatDialog Mock
    • provideMatSnackBarMock(overrides?) - MatSnackBar Mock
    • provideAngularMaterialMocks() - All Material Services

    πŸ› οΈ Custom Services

    3-Line Rule for New Services

    // 1. Define service defaults
    const MY_SERVICE_DEFAULTS: Partial<jest.Mocked<MyService>> = {
      getData: jest.fn(() => of([])),
      saveData: jest.fn(() => Promise.resolve())
    };
    
    // 2. Create factory
    const createMockMyService = (overrides = {}) => 
      createMockService(MY_SERVICE_DEFAULTS, overrides);
    
    // 3. Export provider
    export const provideMyServiceMock = (overrides = {}) => 
      createMockProvider(MyService, createMockMyService(overrides));

    οΏ½ Common Issues & Solutions

    Window/Document Injection Problems

    Error: Ι΅NotFound: NG0201: No provider found for Window

    Quick Fix:

    // ❌ Wrong
    TestBed.inject(Window);
    TestBed.inject(Document);
    
    // βœ… Correct
    import { WINDOW_TOKEN, DOCUMENT_TOKEN } from '@halverscheid-fiae.de/angular-testing-factory';
    
    TestBed.inject(WINDOW_TOKEN);
    TestBed.inject(DOCUMENT_TOKEN);

    Complete Solution:

    import { provideCompleteWindowMock } from '@halverscheid-fiae.de/angular-testing-factory';
    
    describe('MyComponent', () => {
      let cleanup: (() => void) | undefined;
    
      beforeEach(() => {
        const { providers, cleanup: windowCleanup } = provideCompleteWindowMock({
          mockGlobal: true
        });
        cleanup = windowCleanup;
    
        TestBed.configureTestingModule({
          providers: [...providers, /* other providers */]
        });
      });
    
      afterEach(() => cleanup?.());
    });

    FormBuilder Validation Errors

    Error: TypeError: control.setParent is not a function

    Root Cause: Jest needs to properly mock Angular Forms globally to avoid conflicts.

    Complete Solution:

    1. Create jest.setup.js in your project root:
    // jest.setup.js - Global Angular Forms Mock
    jest.mock('@angular/forms', () => {
      const originalModule = jest.requireActual('@angular/forms');
      
      class MockFormControl {
        constructor(formState, validatorOrOpts, asyncValidator) {
          this.value = Array.isArray(formState) ? formState[0] : formState;
          this.valid = true;
          this.invalid = false;
          this.errors = null;
          this.setValue = jest.fn();
          this.patchValue = jest.fn();
          this.reset = jest.fn();
          this.setParent = jest.fn(); // CRITICAL: This method must exist!
          // Additional FormControl properties as needed
        }
      }
    
      class MockFormGroup {
        constructor(controlsConfig, options) {
          this.controls = {};
          this.value = {};
          
          if (controlsConfig) {
            Object.keys(controlsConfig).forEach(key => {
              const config = controlsConfig[key];
              this.controls[key] = new MockFormControl(config);
              // Ensure parent relationship
              if (this.controls[key].setParent) {
                this.controls[key].setParent = jest.fn();
              }
            });
          }
          
          this.setValue = jest.fn();
          this.patchValue = jest.fn();
          this.get = jest.fn((path) => this.controls[path] || null);
          this.setParent = jest.fn();
        }
      }
    
      class MockFormBuilder {
        control(formState, validatorOrOpts, asyncValidator) {
          return new MockFormControl(formState, validatorOrOpts, asyncValidator);
        }
        group(controlsConfig, options) {
          return new MockFormGroup(controlsConfig, options);
        }
      }
    
      return {
        ...originalModule,
        FormControl: MockFormControl,
        FormGroup: MockFormGroup,
        FormBuilder: MockFormBuilder
      };
    });
    1. Update jest.config.js:
    // jest.config.js
    module.exports = {
      setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
      // ... rest of your configuration
    };
    1. Alternative: Use our pre-configured provider:
    import { provideAngularCommon } from 'angular-testing-factory';
    
    TestBed.configureTestingModule({
      providers: [
        provideAngularCommon(), // Includes setParent-aware FormBuilder
        // ... your other providers
      ]
    });

    SignalStore Testing

    Error: Cannot spy on SignalStore methods directly

    Solution:

    οΏ½πŸ” SignalStore Testing

    // ❌ This does NOT work with SignalStores:
    const spy = jest.spyOn(store.myService, 'getData'); // Error!
    
    // βœ… Correct approach for SignalStores:
    const mockService = TestBed.inject(MyService); // After TestBed setup
    const spy = jest.spyOn(mockService, 'getData');

    πŸ”§ Troubleshooting Guide

    Missing Provider Errors

    • Use WINDOW_TOKEN instead of Window
    • Use DOCUMENT_TOKEN instead of Document
    • Ensure all mocks are provided in TestBed configuration

    Mock Not Working

    • Check import statements - ensure you're importing from the correct package
    • Verify TypeScript configuration allows proper jest mocking
    • Use provideCompleteWindowMock for complex window scenarios

    Performance Issues

    • Use setupGlobalWindowMock only when necessary
    • Clean up global mocks in afterEach() hooks
    • Consider using token-based injection for better performance

    πŸ’‘ Why This Library?

    Before (Traditional Approach)

    // ❌ Error-prone, lots of boilerplate, mock drift
    const mockService = {
      getData: jest.fn(),
      setData: jest.fn(),
      // Forgotten methods lead to runtime errors
    };
    
    TestBed.configureTestingModule({
      providers: [
        { provide: MyService, useValue: mockService }
      ]
    });

    After (With Angular Testing Factory)

    // βœ… Type-safe, 3-line rule, zero mock drift
    TestBed.configureTestingModule({
      providers: [
        provideMyServiceMock({
          getData: jest.fn(() => of(customData))
        })
      ]
    });

    πŸ—οΈ Architecture

    @halverscheid-fiae.de/angular-testing-factory/
    β”œβ”€β”€ 🏭 core/           # Universal Mock Factory System
    β”œβ”€β”€ πŸ“¦ presets/        # Ready-to-use Service Mocks
    β”œβ”€β”€ 🎯 types/          # TypeScript Definitions
    └── πŸ› οΈ utils/          # Test Helper Utilities

    🎯 Problem Solved

    Traditional Angular testing suffers from:

    • Mock Drift: Service changes break tests at runtime
    • Boilerplate: Repetitive mock setup code
    • Type Safety: Missing compile-time guarantees
    • SignalStore Issues: Complex injection context handling

    This library provides:

    • Compile-time Safety: TypeScript satisfies catches errors early
    • DRY Principle: Reusable factories eliminate duplication
    • Modern Angular: Built for standalone components and signals
    • Developer Experience: 3-line rule for maximum productivity

    πŸ“‹ Requirements

    • Angular 20+
    • TypeScript 5.0+
    • Jest 29+
    • RxJS 7+

    🀝 Contributing

    Contributions welcome! Please read our Contributing Guide.

    Development Workflow

    1. Fork the repository
    2. Create your feature branch (git checkout -b feature/amazing-feature)
    3. Commit your changes following our commit conventions (see below)
    4. Push to the branch (git push origin feature/amazing-feature)
    5. Open a Pull Request

    πŸ“‹ Commit Message Conventions

    This project uses automatic semantic versioning based on commit messages. Please follow these conventions:

    Version Bumping Rules

    πŸ”§ Patch Release (1.0.0 β†’ 1.0.1):

    git commit -m "fix: resolve HttpClient mock timeout issue"
    git commit -m "docs: update installation instructions"  
    git commit -m "chore: update dependencies"

    ✨ Minor Release (1.0.0 β†’ 1.1.0):

    git commit -m "feat: add MatSnackBar mock provider"
    git commit -m "feat(presets): add Angular Forms mock collection"

    πŸ’₯ Major Release (1.0.0 β†’ 2.0.0):

    git commit -m "feat!: redesign API for better TypeScript inference"
    git commit -m "refactor!: remove deprecated functions"
    
    # Or with BREAKING CHANGE in body:
    git commit -m "feat: redesign API for better TypeScript inference
    
    BREAKING CHANGE: createMockProvider now requires explicit type parameter"

    Commit Types

    • feat: New features β†’ Minor version
    • fix: Bug fixes β†’ Patch version
    • docs: Documentation β†’ Patch version
    • style: Code style β†’ Patch version
    • refactor: Code refactoring β†’ Patch version
    • test: Adding tests β†’ Patch version
    • chore: Maintenance β†’ Patch version

    Breaking Changes

    Add BREAKING CHANGE: in commit body OR use ! after type for Major version:

    # Option 1: ! suffix (recommended)
    git commit -m "feat!: remove deprecated createLegacyMock function"  
    git commit -m "refactor!: change API structure"
    
    # Option 2: BREAKING CHANGE in body
    git commit -m "refactor: improve type inference
    
    BREAKING CHANGE: Generic type parameters order changed"

    πŸ€– Automatic Publishing

    When your PR is merged to main:

    1. βœ… Version automatically bumped based on commit messages
    2. βœ… Git tag created (e.g., v1.2.3)
    3. βœ… NPM package published automatically
    4. βœ… No manual steps required!

    Example Workflow:

    • You commit: feat: add new provider for Angular Router
    • After merge: 1.0.0 β†’ 1.1.0 + NPM publish + Git tag v1.1.0

    πŸ› Issues

    Found a bug? Please report it.

    πŸ“„ License

    MIT Β© Christian Halverscheid

    πŸš€ Made with ❀️ for the Angular Community

    This library was created to solve real-world testing challenges in enterprise Angular applications. Your feedback and contributions make it better!