ng-mocks

repository·main·Indexed 22 days ago

https://github.com/help-me-mom/ng-mocks

A library designed to simplify Angular testing by providing utilities to mock components, directives, pipes, modules, services, and tokens. It features MockBuilder for simplifying TestBed configuration, MockRender for handling lifecycle hooks and OnPush change detection, and various helper functions like getMockedNgDefOf and isMockOf to reduce boilerplate and manage mock instances.

Tokens
95K
Snippets
292
Records
333
Agent score
77%

What's inside ng-mocks

  1. Overview of the ngMocks namespace

    main

    The ngMocks namespace is a collection of helper functions designed to simplify Angular testing. It provides tools for:

    • Customizing mock behavior: Controlling how components, services, and other dependencies are mocked.
    • Simulating events: Triggering form control events (like change or touch) and standard HTML events (like click).
    • Manipulating templates: Rendering or hiding ng-template elements.
    • Accessing elements and instances: Finding DOM elements, component instances, and TemplateRef objects within a fixture.
    • Stubbing: Replacing specific methods or properties on objects with stubs.
    • Test helpers: Utilities for managing the console, formatting output, and cleaning up the TestBed.
  2. Understand the limitations of MockModule for testing internals and externals

    main

    When using MockModule(Module), you may encounter issues depending on whether you are trying to test an internal component or an exported component:

    1. Testing Internals: If you try to test a component/directive that is declared in a module but not exported, MockModule will not provide access to it. Even if you mock the module, the testing module won't 'see' the unexported internal members.
    2. Testing Externals: If you try to test an exported component by mocking its parent module, you might run into 'declarations of 2 modules' errors. This happens because MockModule creates a mock version of the exported component, which conflicts if you also declare the real component in your TestBed.

    To avoid these issues, use MockBuilder or ngMocks.guts instead of MockModule for complex module dependencies.

  3. Implement ControlValueAccessor using methods instead of properties

    main

    When creating custom implementations of ControlValueAccessor for testing, you must define the interface methods using standard method syntax rather than arrow function properties.

    Because ng-mocks creates mocks without calling the original constructor, properties defined as arrow functions (e.g., public writeValue = () => {}) will not exist on the mock instance, leading to errors like No value accessor for form control with name ....

    Always use the following pattern:

    export class MyControl implements ControlValueAccessor {
      public writeValue(value: any) { /* ... */ }
      public registerOnChange(fn: any) { /* ... */ }
      public registerOnTouched(fn: any) { /* ... */ }
    }
    export class MyControl implements ControlValueAccessor {
      public writeValue() {
        // some magic
      }
    
      public registerOnChange() {
        // some magic
      }
    
      public registerOnTouched() {
        // some magic
      }
    }
  4. Distinguish between fixture.componentInstance and fixture.point.componentInstance

    main

    When using MockRender, you must choose the correct instance property based on your goal:

    • fixture.componentInstance: Use this to change inputs or outputs. If you provided params to MockRender(Component, params), this property acts as a proxy to those params. Changing it updates the params and vice-versa.
    • fixture.point.componentInstance: Use this to assert expectations. This is the real instance of the component being tested.

    Best Practice:

    • To change values: Use fixture.componentInstance or the params object.
    • To check values: Use fixture.point.componentInstance.
    class Component {
      @Input() public i1: number = 1;
      @Input() public i2: number = 2;
    }
    
    const params = { i1: 5 };
    const fixture = MockRender(Component, params);
    
    // Changing inputs via proxy
    params.i1 = 6; 
    // fixture.componentInstance.i1 is now 6
    
    // Asserting via the real instance
    expect(fixture.point.componentInstance.i1).toEqual(6);
  5. Customize provider dependencies in MockBuilder

    main

    When using MockBuilder, you can customize how dependencies are provided using three different methods: .mock(), .provide(), and MockInstance.

    • .mock(Token, options): Replaces a dependency (like a service) with a mock version. Use this for services that are already part of the module being mocked.
    • .provide(providerConfig): Adds a standard Angular provider to the TestBed. This is useful for providing new values or services that aren't part of the original module.
    • MockInstance(Token, options): Used to customize the behavior of an existing mock instance. This is typically called in a beforeAll block to configure how a mocked class should behave (e.g., overriding a method on a mock).

    Always use MockReset in afterAll if you have used MockInstance to ensure customizations don't leak between tests.

    // 1. Using .mock() to customize a service in the module
    beforeEach(() =>
      MockBuilder(TargetService, TargetModule)
        .mock(Service2, {
          trigger: () => 'mock2',
        })
        // 2. Using .provide() to add a new provider to TestBed
        .provide({
          provide: Service3,
          useValue: {
            trigger: () => 'mock3',
          },
        })
    );
    
    // 3. Using MockInstance() to configure a mock globally for the suite
    beforeAll(() => {
      MockInstance(Service1, {
        init: instance => {
          instance.trigger = () => 'mock1';
        },
      });
    });
    
    // Cleanup
    afterAll(MockReset);
  6. Understand the MockedComponentFixture return type

    main

    When using MockRender(Component), the returned fixture is not a standard ComponentFixture<T>, but a MockedComponentFixture<T>.

    This type includes an additional point property. The fixture.point.componentInstance is typed to the actual class being rendered and supports components, directives, services, and tokens. This is necessary because MockRender generates an internal wrapper component to manage bindings.

  7. Render deeply nested templates

    main

    You can render a TemplateRef or structural directive at any depth within a component tree. The only requirement is that there must be a valid chain of queries (e.g., ContentChild, ContentChildren) that allows the starting instance to reach the target template.

    // If 'icon' is a nested template inside 'xd-cell' which is inside 'xd-header'...
    // and 'xdCard' has queries to reach that depth:
    ngMocks.render(xdCard, icon);
  8. How MockRender handles params, inputs, and outputs

    main

    When using MockRender(Component, params), the params object controls the generated template. MockRender creates a wrapper component that acts as a proxy between your params and the tested component's inputs and outputs.

    Behavior based on params content:

    1. No params (null, undefined, or omitted): MockRender automatically binds all inputs to null and ignores all outputs. This mimics Angular's behavior when an optional chain fails.

      • Template: <my-component [input1]="input1" [input2]="input2"></my-component> where input1 and input2 are null.
    2. Empty params ({}): MockRender does not bind any inputs or outputs. This allows the tested component to use its own default values.

      • Template: <my-component></my-component>.
      • To update values in this mode, you must modify the instance of the tested component directly via fixture.point.componentInstance.
    3. Provided params ({ key: value }): Only keys in params that match an input or output name are included in the template.

      • Inputs: Generates [propName]="propName". Changing a value in the params object and calling fixture.detectChanges() will update the tested component.
      • Outputs: MockRender detects the type of the provided value and generates appropriate template logic:
        • function: (outputName)="outputName($event)"
        • EventEmitter: (outputName)="outputName.emit($event)"
        • Subject: (outputName)="outputName.next($event)"
        • literal: (outputName)="outputName=$event"
    // Example of provided params for inputs and outputs
    const params = {
      o1: undefined, // literal/assignment
      o2: jasmine.createSpy('o2'), // function
      o3: new EventEmitter(), // EventEmitter
      o4: new Subject(), // Subject
    };
    const fixture = MockRender(MyComponent, params);
    
    // Triggering outputs
    fixture.point.componentInstance.o1.emit(1);
    fixture.point.componentInstance.o2.emit(2);
    
    expect(params.o1).toEqual(1);
    expect(params.o2).toHaveBeenCalledWith(2);
  9. Access the tested component instance via fixture.point

    main

    When using MockRender, the fixture.componentInstance refers to the wrapper component generated by ng-mocks. To access the actual instance of the component, directive, pipe, or service you are testing, use fixture.point.componentInstance.

    • Component/Directive: fixture.point.componentInstance is the instance of the tested entity.
    • Pipe: fixture.point.componentInstance is the instance of the pipe.
    • Service/Token: fixture.point.componentInstance is the value of the token.
    // For a component
    const fixture = MockRender(AppComponent);
    // The actual AppComponent instance:
    const appInstance = fixture.point.componentInstance;
    
    // For a pipe
    const fixture = MockRender(DatePipe, { $implicit: new Date() });
    // The actual DatePipe instance:
    const pipeInstance = fixture.point.componentInstance;
  10. Understand Flex mode vs Strict mode in MockBuilder

    main

    MockBuilder offers two modes for configuring dependencies:

    Flex mode

    In Flex mode, you call MockBuilder() without arguments (or with only one) and explicitly define every dependency's state using .keep() or .mock(). This gives you full control but can lead to tests that pass even if the actual application's dependency tree changes.

    // Flex mode example
    beforeEach(() => {
      return MockBuilder()
        .keep(TargetComponent)
        .mock(CurrencyPipe)
        .mock(TimeService)
        .keep(ReactiveFormModule);
    });

    Strict mode is enabled by passing two parameters to MockBuilder:

    1. The item to be kept as-is (the subject under test).
    2. The module (or array of modules) that contains the dependencies.

    In Strict mode, MockBuilder automatically mocks everything declared or imported in the provided module(s). If a dependency is missing from the provided module, the test will fail. This ensures your tests stay in sync with your actual application code.

    // Strict mode example
    beforeEach(() => {
      return MockBuilder(TargetComponent, TargetModule)
        .keep(ReactiveFormModule); // Keeps ReactiveFormModule and fails if it's missing from TargetModule
    });

    To handle lazy-loaded modules or multiple root modules, pass an array as the second parameter:

    beforeEach(() => {
      return MockBuilder(
        TargetComponent,
        [TargetModule, AppModule]
      ).keep(CurrencyPipe);
    });
    return MockBuilder(TargetComponent, TargetModule).keep(ReactiveFormModule);
  11. Manage MockInstance customization scopes

    main

    To avoid manual resets of mock customizations between tests or suites, use MockInstance's scoping mechanisms.

    Manual Checkpoints

    • MockInstance.remember(): Creates a checkpoint. Customizations made after this call are recorded separately.
    • MockInstance.restore(): Discards all customizations made since the last remember() checkpoint.

    Automatic Scoping with MockInstance.scope()

    MockInstance.scope() provides a shorthand for managing these checkpoints:

    • MockInstance.scope('suite'): Uses beforeAll and afterAll (applies to the current suite and all children).
    • MockInstance.scope(): Uses beforeEach and afterEach (applies only to the current spec/test).
    • MockInstance.scope('all'): Uses both beforeAll/afterAll AND beforeEach/afterEach.
    describe('suite', () => {
      // Automatically handles remember/restore for the whole suite
      MockInstance.scope('suite');
    
      describe('sub suite', () => {
        // Automatically handles remember/restore for each test in this sub suite
        MockInstance.scope();
        
        it('test', () => {
          // Customization here only affects this test
          MockInstance(SomeService, 'method', () => 'local');
        });
      });
    });
  12. Exclude all guards using NG_MOCKS_GUARDS

    main

    When testing a specific routing guard, other guards in your application might block the route due to their mocked implementations returning falsy values. To ensure you are testing only the guard you intend to, use the NG_MOCKS_GUARDS token with the .exclude() method in MockBuilder. This removes all other guards from the TestBed, preventing side effects from unrelated guard logic.

    MockBuilder(TargetModule)
      .exclude(NG_MOCKS_GUARDS)
      .keep(MySpecificGuard);