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 about ControlValueAccessor
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
}
}
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
.
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.
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
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');
});
});