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
π― 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 satisfiescatches 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-factory90% 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 testsCommon 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.innerWidthin legacy components
- Global mocking: Testing code that accesses windowdirectly
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:
- 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
  };
});- Update jest.config.js:
// jest.config.js
module.exports = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  // ... rest of your configuration
};- 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_TOKENinstead ofWindow
- Use DOCUMENT_TOKENinstead ofDocument
- 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 provideCompleteWindowMockfor complex window scenarios
Performance Issues
- Use setupGlobalWindowMockonly 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 satisfiescatches 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
- Fork the repository
- Create your feature branch (git checkout -b feature/amazing-feature)
- Commit your changes following our commit conventions (see below)
- Push to the branch (git push origin feature/amazing-feature)
- 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:
- β Version automatically bumped based on commit messages
- β
 Git tag created (e.g., v1.2.3)
- β NPM package published automatically
- β 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 tagv1.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!