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

Caution about ControlValueAccessor

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
}
}

Caution about ngModel

It's important to call fixture.whenStable() in addition to fixture.detectChanges() if FormsModule is kept in a test to let ngModel udpate its values correctly.

Because fixture.whenStable() returns a promise, the whole test should be async.

wait until fixture is stable

After calling fixture.detectChanges(), make sure to call await fixture.whenStable().

it('changes ngModel values', async () => { // <-- make `it` async
// MockRender calls detectChanges by default.
const fixture = MockRender(TargetComponent);
await fixture.whenStable(); // <-- let ngModel render inputs correctly

// ... assert old value

ngMocks.change('.input'', 'newValue');
fixture.detectChanges(); // <-- let ngModel render inputs correctly
await fixture.whenStable(); // <-- let ngModel render inputs correctly

// ... assert new value
});

Advanced example

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

https://github.com/help-me-mom/ng-mocks/blob/master/examples/MockReactiveForms/test.spec.ts
describe('MockReactiveForms', () => {
// Helps to reset MockInstance customizations after each test.
MockInstance.scope();

beforeEach(() => {
// DependencyComponent is a declaration in ItsModule.
return (
MockBuilder(TargetComponent, ItsModule)
// ReactiveFormsModule is an import in ItsModule.
.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'); // or jest.fn();
// 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(CvaComponent, 'writeValue', writeValue);

const fixture = MockRender(TargetComponent);
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(CvaComponent);
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

https://github.com/help-me-mom/ng-mocks/blob/master/examples/MockForms/test.spec.ts
describe('MockForms', () => {
// Helps to reset customizations after each test.
// Alternatively, you can enable
// automatic resetting in test.ts.
MockInstance.scope();

beforeEach(() => {
// DependencyComponent is a declaration in ItsModule.
return (
MockBuilder(TargetComponent, ItsModule)
// FormsModule is an import in ItsModule.
.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'); // or jest.fn();
// 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(CvaComponent, 'writeValue', writeValue);

const fixture = MockRender(TargetComponent);
// FormsModule needs fixture.whenStable()
// right after MockRender to install all hooks.
await fixture.whenStable();
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(CvaComponent);
ngMocks.change(mockControlEl, 'foo');
expect(component.value).toBe('foo');

// Let's check that change on existing value
// causes calls of `writeValue` on the mock component.
component.value = 'bar';
// Both below are needed to trigger writeValue.
fixture.detectChanges();
await fixture.whenStable();
expect(writeValue).toHaveBeenCalledWith('bar');
});
});