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:
ngModelngModelChangeformControlformControlName
NG_VALUE_ACCESSORControlValueAccessorwriteValueregisterOnChangeregisterOnTouched
NG_VALIDATORSValidatorNG_ASYNC_VALIDATORSAsyncValidatorregisterOnValidatorChangevalidate
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 ....
export class MyControl implements ControlValueAccessor {
public writeValue = () => {
// some magic
};
public registerOnChange = () => {
// some magic
};
public registerOnTouched = () => {
// some magic
};
}
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', () => {
// Helps to reset MockInstance customizations after each test.
MockInstance.scope();
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', () => {
// Helps to reset customizations after each test.
MockInstance.scope();
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);
// 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(DependencyComponent);
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');
});
});