Skip to main content

How to mock form controls in Angular tests

ng-mocks respects ControlValueAccessor interface if a directive, or a component implements it. Apart from that, ng-mocks provides helper functions to emit changes and touches.

it supports both FormsModule and ReactiveFormsModule:

  • ngModel
  • ngModelChange
  • formControl
  • formControlName
  • NG_VALUE_ACCESSOR
  • ControlValueAccessor
  • writeValue
  • registerOnChange
  • registerOnTouched
  • NG_VALIDATORS
  • Validator
  • NG_ASYNC_VALIDATORS
  • AsyncValidator
  • registerOnValidatorChange
  • validate

Related tools#

Caution#

Use methods instead of properties

It is important to implement ControlValueAccessor via methods

Otherwise, there is no way to detect such properties via javascript interfaces, because the properties don't exist without a constructor call, whereas mocks don't call original constructors.

Usually, if you are using properties in a test, you would get No value accessor for form control with name ....

Wrong definition via properties
export class MyControl implements ControlValueAccessor {    public writeValue = () => {      // some magic  };    public registerOnChange = () => {    // some magic  };    public registerOnTouched = () => {    // some magic  };}
Correct definition via methods
export class MyControl implements ControlValueAccessor {  public writeValue() {    // some magic  }
  public registerOnChange() {    // some magic  }
  public registerOnTouched() {    // some magic  }}

Advanced example#

An advanced example of a mock FormControl with ReactiveForms in Angular tests. Please, pay attention to comments in the code.

describe('MockReactiveForms', () => {  beforeEach(() => {    return MockBuilder(TestedComponent)      .mock(DependencyComponent)      .keep(ReactiveFormsModule);  });
  it('sends the correct value to the mock form component', () => {    // That is our spy on writeValue calls.    // With auto spy this code is not needed.    const writeValue = jasmine.createSpy('writeValue');    // in case of jest    // const writeValue = jest.fn();
    // Because of early calls of writeValue, we need to install    // the spy via MockInstance before the render.    MockInstance(DependencyComponent, 'writeValue', writeValue);
    const fixture = MockRender(TestedComponent);    const component = fixture.point.componentInstance;
    // During initialization it should be called    // with null.    expect(writeValue).toHaveBeenCalledWith(null);
    // Let's find the form control element    // and simulate its change, like a user does it.    const mockControlEl = ngMocks.find(DependencyComponent);    ngMocks.change(mockControlEl, 'foo');    expect(component.formControl.value).toBe('foo');
    // Let's check that change on existing formControl    // causes calls of `writeValue` on the mock component.    component.formControl.setValue('bar');    expect(writeValue).toHaveBeenCalledWith('bar');  });});

A usage example of mock FormControl with ngModel in Angular tests

describe('MockForms', () => {  beforeEach(() => {    return MockBuilder(TestedComponent)      .mock(DependencyComponent)      .keep(FormsModule);  });
  it('sends the correct value to the mock form component', async () => {    // That is our spy on writeValue calls.    // With auto spy this code is not needed.    const writeValue = jasmine.createSpy('writeValue');    // in case of jest    // const writeValue = jest.fn();
    // Because of early calls of writeValue, we need to install    // the spy via MockInstance before the render.    MockInstance(DependencyComponent, 'writeValue', writeValue);
    const fixture = MockRender(TestedComponent);    const component = fixture.point.componentInstance;
    // During initialization it should be called    // with null.    expect(writeValue).toHaveBeenCalledWith(null);
    // Let's find the form control element    // and simulate its change, like a user does it.    const mockControlEl = ngMocks.find(DependencyComponent);    ngMocks.change(mockControlEl, 'foo');    await fixture.whenStable();    expect(component.value).toBe('foo');
    // Let's check that change on existing value    // causes calls of `writeValue` on the mock component.    component.value = 'bar';    fixture.detectChanges();    await fixture.whenStable();    expect(writeValue).toHaveBeenCalledWith('bar');  });});