Skip to main content

How to mock signal form controls in Angular tests

Mock a custom signal control when testing how its parent exchanges values with it. Keep FormField real so the parent form still connects to the mocked child. Signal forms require Angular 21 or newer.

This parent binds its inputValue field to a child that implements FormValueControl:

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

@Component({
selector: 'mock-forms-input-control',
template: `
<input [value]="value()" (input)="value.set($any($event.target).value)" />
`,
})
class InputControl implements FormValueControl<string> {
public readonly value = model('');
}

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

Test setup

Configure the testing module in beforeEach with MockBuilder. Keep the parent and form binding real, and mock InputControl:

beforeEach(() =>
MockBuilder(TargetComponent)
// Preserve the parent form connection while replacing the child.
.keep(FormField)
.keep(NG_MOCKS_ROOT_PROVIDERS)
.mock(InputControl),
);

Keeping NG_MOCKS_ROOT_PROVIDERS preserves the services used by the real form binding. Mocking FormField itself would remove the connection between the parent form and the control.

Testing the parent binding

Changing and touching are separate

ngMocks.change updates the custom control's value and marks its field dirty. It does not mark the field touched. A control must expose a touch binding to support the separate touch interaction.

Inside it, render the parent with MockRender, then find the mocked child with ngMocks.find. Read the parent's model and the child's value() signal, then pass InputControl directly to ngMocks.change:

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

// Find the child.
const host = ngMocks.find(InputControl);
const control = ngMocks.get(host, InputControl);

// Read the value.
expect(component.model().inputValue).toBe('Ada');
expect(control.value()).toBe('Ada');

// Change the value.
ngMocks.change(InputControl, 'Grace');
// or ngMocks.change(host, 'Grace');
// Propagate the updated field value back to the child's model input.
fixture.detectChanges();

// Assert the result.
expect(component.model().inputValue).toBe('Grace');
expect(control.value()).toBe('Grace');

Parent model writes

A parent write also reaches the mocked child on the next change-detection pass:

// Change the parent model.
component.model.set({ inputValue: 'Katherine' });
// Deliver the parent value to the mocked child.
fixture.detectChanges();

// Assert the result.
expect(component.model().inputValue).toBe('Katherine');
expect(control.value()).toBe('Katherine');

Checkbox controls

A FormCheckboxControl exposes a boolean checked model. Pass true or false to change the mocked child's checked state:

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

@Component({
selector: 'mock-forms-checkbox-control',
template: `
<input
type="checkbox"
[checked]="checked()"
(change)="checked.set($any($event.target).checked)"
/>
`,
})
class CheckboxControl implements FormCheckboxControl {
public readonly checked = model(false);
}

@Component({
selector: 'target-mock-forms-checkbox-signals',
imports: [FormField, CheckboxControl],
template: '<mock-forms-checkbox-control [formField]="f.checkboxValue" />',
})
class TargetComponent {
public readonly model = signal({ checkboxValue: false });
public readonly f = form(this.model);
}

For this variant, mock CheckboxControl in the setup:

beforeEach(() =>
MockBuilder(TargetComponent)
// Preserve the parent form connection while replacing the child.
.keep(FormField)
.keep(NG_MOCKS_ROOT_PROVIDERS)
.mock(CheckboxControl),
);

Render the parent as above, then find the child and change its checked state:

// Find the child.
const host = ngMocks.find(CheckboxControl);
const control = ngMocks.get(host, CheckboxControl);

// Read the checked state.
expect(component.model().checkboxValue).toBe(false);
expect(control.checked()).toBe(false);

// Check the control.
ngMocks.change(CheckboxControl, true);
// or ngMocks.change(host, true);
fixture.detectChanges();

// Assert the result.
expect(component.model().checkboxValue).toBe(true);
expect(control.checked()).toBe(true);

// Uncheck the control.
ngMocks.change(CheckboxControl, false);
// or ngMocks.change(host, false);
fixture.detectChanges();

// Assert the result.
expect(component.model().checkboxValue).toBe(false);
expect(control.checked()).toBe(false);

Touch

In Angular 22, a custom control can expose a touch output and a touched input. ngMocks.touch emits the child's touch event. After change detection, the child receives the updated touched state while its value stays unchanged. The touch example includes that control declaration and its tests.

Live example

Here is the complete test for the value control. The executable spec also checks that the field binding identifies the same mocked child.

https://github.com/help-me-mom/ng-mocks/blob/main/examples/MockForms/signals.spec.ts
import { Component, model, signal } from '@angular/core';
import { form, FormField, FormValueControl } from '@angular/forms/signals';

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

@Component({
selector: 'mock-forms-input-control',
template: `
<input [value]="value()" (input)="value.set($any($event.target).value)" />
`,
})
class InputControl implements FormValueControl<string> {
public readonly value = model('');
}

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

describe('MockForms:signals', () => {
beforeEach(() =>
MockBuilder(TargetComponent)
// Preserve the parent form connection while replacing the child.
.keep(FormField)
.keep(NG_MOCKS_ROOT_PROVIDERS)
.mock(InputControl),
);

it('passes values between the parent and the mocked signal control', () => {
// Render the parent.
const fixture = MockRender(TargetComponent);
const component = fixture.point.componentInstance;

// Find the child.
const host = ngMocks.find(InputControl);
const control = ngMocks.get(host, InputControl);

// Read the value.
expect(component.model().inputValue).toBe('Ada');
expect(control.value()).toBe('Ada');

// Change the value.
ngMocks.change(InputControl, 'Grace');
// or ngMocks.change(host, 'Grace');
// Propagate the updated field value back to the child's model input.
fixture.detectChanges();

// Assert the result.
expect(component.model().inputValue).toBe('Grace');
expect(control.value()).toBe('Grace');

// Change the parent model.
component.model.set({ inputValue: 'Katherine' });
// Deliver the parent value to the mocked child.
fixture.detectChanges();

// Assert the result.
expect(component.model().inputValue).toBe('Katherine');
expect(control.value()).toBe('Katherine');
});
});

Checkbox example

The complete checkbox spec also checks parent writes and emitted checked values.

Touch example

The complete Angular 22 touch spec checks changes and touch independently, including the parent's rendered value.