Skip to main content

How to mock Angular form bindings

Mock form directives to test the values and output bindings supplied by a parent component. For example, this component passes inputValue to NgModel and receives its ngModelChange output:

import { Component, NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
selector: 'target-mock-form-bindings',
standalone: false,
template: `
<input name="inputName" [(ngModel)]="inputValue" />
`,
})
class TargetComponent {
public inputValue = 'Ada';
}

@NgModule({
declarations: [TargetComponent],
imports: [FormsModule],
})
class TargetModule {}

Test setup

Pass the component and its module to MockBuilder. This keeps TargetComponent real and mocks its dependencies, including the form directives:

beforeEach(() => MockBuilder(TargetComponent, TargetModule));

Testing the binding

Render with MockRender, find the input with ngMocks.find, and read the mocked directive's input with ngMocks.input. ngMocks.change emits the mocked NgModel directive's ngModelChange output, which updates the parent property:

// Render the component.
const fixture = MockRender(TargetComponent);
const component = fixture.point.componentInstance;

// Find the input to inspect its mocked binding and native value.
const input = ngMocks.find('[name="inputName"]');

// Read the binding.
expect(ngMocks.input(input, 'ngModel')).toBe('Ada');
expect(component.inputValue).toBe('Ada');
expect(input.nativeElement.value).toBe('');

// Emit the mocked ngModelChange output to update the parent property.
ngMocks.change('[name="inputName"]', 'Grace');
// or ngMocks.change(input, 'Grace');
fixture.detectChanges();

// Assert the result.
expect(component.inputValue).toBe('Grace');
expect(ngMocks.input(input, 'ngModel')).toBe('Grace');

The initial native value is empty because the mocked directive does not connect the component property to the DOM. A later parent write updates the mock's input when the component is checked, but does not update the native value. After assigning a parent property directly, mark its view for checking before fixture.detectChanges(), as shown in the executable spec. To test the native connection and control state, keep the forms module real as shown in the ngModel and reactive forms guides.

Reactive controls

For <input [formControl]="inputValue">, create inputValue = new FormControl('Ada') in the component and import ReactiveFormsModule in its module. Use the same MockBuilder(TargetComponent, TargetModule) setup to mock the form directives.

The mocked FormControlDirective receives the real control object created by the component. Find that input with ngMocks.reveal. ngMocks.change calls setValue on the supplied control; its valueChanges subscribers still receive the update:

// Render the component.
const fixture = MockRender(TargetComponent);
const component = fixture.point.componentInstance;

// Find the input.
const input = ngMocks.reveal(['formControl', component.inputValue]);

// Read the binding and its real control value.
expect(ngMocks.input(input, 'formControl')).toBe(component.inputValue);
expect(component.inputValue.value).toBe('Ada');

// Change the supplied FormControl.
ngMocks.change(input, 'Grace');

// Assert the result.
expect(component.inputValue.value).toBe('Grace');
expect(ngMocks.input(input, 'formControl')).toBe(component.inputValue);

This does not restore the directive's native input connection. Later calls to the parent control's setValue update the control, but leave the native input unchanged.

Named controls and groups

A mocked FormGroupDirective receives the group's reference, and a mocked FormControlName receives the control's name:

<form [formGroup]="form">
<input formControlName="inputValue" />
</form>
public readonly form = new FormGroup({
inputValue: new FormControl('Ada'),
});

With MockBuilder(TargetComponent, TargetModule), the mocked directives do not resolve that name to the control in the group. The input has a mocked value accessor, so ngMocks.change changes its native value while leaving the group unchanged:

// Render the component.
const fixture = MockRender(TargetComponent);
const component = fixture.point.componentInstance;

// Find the bound group and named input.
const form = ngMocks.reveal(['formGroup', component.form]);
const input = ngMocks.reveal(['formControlName', 'inputValue']);

// Read the bindings.
expect(ngMocks.input(form, 'formGroup')).toBe(component.form);
expect(ngMocks.input(input, 'formControlName')).toBe('inputValue');

// Change the native input through its mocked value accessor.
ngMocks.change(input, 'Grace');
fixture.detectChanges();

// Assert that the DOM change did not update the group.
expect(component.form.value).toEqual({ inputValue: 'Ada' });
expect(ngMocks.input(form, 'formGroup')).toBe(component.form);
expect(ngMocks.input(input, 'formControlName')).toBe('inputValue');
expect(input.nativeNode.value).toBe('Grace');

Signal fields

With Angular 21 or later, a mocked FormField receives the real field tree created by the parent. This component passes f.inputValue to the directive:

import { Component, signal } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';

@Component({
selector: 'target-mock-form-bindings-signals',
imports: [FormField],
template: '<input [formField]="f.inputValue" />',
})
class TargetComponent {
public readonly model = signal({ inputValue: 'Ada' });
public readonly f = form(this.model);
}

Mock the directive in the setup:

beforeEach(() => MockBuilder(TargetComponent).mock(FormField));

Find the input with ngMocks.reveal and reuse the returned host. ngMocks.change updates the supplied field and marks it dirty. Use ngMocks.touch separately to mark it touched:

// Render the component.
const fixture = MockRender(TargetComponent);
const component = fixture.point.componentInstance;

// Find the input by its field tree.
const input = ngMocks.reveal(['formField', component.f.inputValue]);

// Read the binding and the parent model.
expect(ngMocks.input(input, 'formField')).toBe(component.f.inputValue);
expect(component.model()).toEqual({ inputValue: 'Ada' });
expect(input.nativeNode.value).toBe('');
expect(component.f.inputValue().dirty()).toBe(false);
expect(component.f.inputValue().touched()).toBe(false);

// Change the supplied field without restoring the mocked DOM connection.
ngMocks.change(input, 'Grace');

// Assert the model update and dirty state; changing the binding does not touch it.
expect(component.model()).toEqual({ inputValue: 'Grace' });
expect(ngMocks.input(input, 'formField')).toBe(component.f.inputValue);
expect(input.nativeNode.value).toBe('');
expect(component.f.inputValue().dirty()).toBe(true);
expect(component.f.inputValue().touched()).toBe(false);

// Touch the supplied field explicitly without dispatching a native blur event.
ngMocks.touch(input);

expect(component.f.inputValue().touched()).toBe(true);
expect(component.f.inputValue().dirty()).toBe(true);
expect(component.model()).toEqual({ inputValue: 'Grace' });
expect(ngMocks.input(input, 'formField')).toBe(component.f.inputValue);
expect(input.nativeNode.value).toBe('');

Pass f.inputValue itself to reveal, rather than the field state returned by f.inputValue(). The helper calls the field's controlValue.set, so Angular's debounce policy still applies. With debounce(path, 'blur'), the edit stays pending until ngMocks.touch calls markAsTouched and flushes it. Touching a pristine field alone does not change its value or mark it dirty.

These operations update the supplied field without synchronizing native values, calling a custom control's writeValue, or connecting its model inputs and outputs. Later parent writes also leave the native input unchanged. Keep FormField real to test the complete Angular connection, as shown in the signal forms guide.

Live examples

Each variant has a complete executable spec:

Here is the complete primary NgModel example. Its executable spec also checks later parent writes.

https://github.com/help-me-mom/ng-mocks/blob/main/examples/MockFormBindings/test.spec.ts
import { Component, NgModule } from '@angular/core';
import { FormsModule, NgModel } from '@angular/forms';

import { isMockOf, MockBuilder, MockRender, ngMocks } from 'ng-mocks';

@Component({
selector: 'target-mock-form-bindings',
standalone: false,
template: `
<input name="inputName" [(ngModel)]="inputValue" />
`,
})
class TargetComponent {
public inputValue = 'Ada';
}

@NgModule({
declarations: [TargetComponent],
imports: [FormsModule],
})
class TargetModule {}

describe('MockFormBindings', () => {
beforeEach(() => MockBuilder(TargetComponent, TargetModule));

it('reads a mocked ngModel input and updates its parent binding', () => {
// Render the component.
const fixture = MockRender(TargetComponent);
const component = fixture.point.componentInstance;

// Find the input to inspect its mocked binding and native value.
const input = ngMocks.find('[name="inputName"]');

// Read the binding.
expect(isMockOf(ngMocks.get(input, NgModel), NgModel)).toBe(true);
expect(ngMocks.input(input, 'ngModel')).toBe('Ada');
expect(component.inputValue).toBe('Ada');
expect(input.nativeElement.value).toBe('');

// Emit the mocked ngModelChange output to update the parent property.
ngMocks.change('[name="inputName"]', 'Grace');
// or ngMocks.change(input, 'Grace');
fixture.detectChanges();

// Assert the result.
expect(component.inputValue).toBe('Grace');
expect(ngMocks.input(input, 'ngModel')).toBe('Grace');
});
});