How do I test Signal & Model Inputs?
Tags: angular, testing, webdev, javascript
Published: 2024-03-08 16:00:00

This article explains how to test Signal and Model Inputs, which are the `input()` and the `model()` functions.

---

## Signal Inputs

Signal Inputs arrived in Angular 17.1. They fulfill the same tasks as the `@Input` decorator: Property Binding. A Signal Input is a simple function named `input()`. The new Property Binding syntax looks like this:

```typescript
// @Input-Style (old)
class HolidayComponent {
  @Input() username = '';
  @Input({required: true}) holiday: Holiday | undefined;
}

// Signal Input (new)
class HolidayComponent {
  username = input(''); // Signal<string>
  holiday = input<Holiday>(); // Signal<Holiday | undefined>
} 
```

The property is of type `Signal`, which makes it reactive by nature. Instead of `ngOnChanges()` and `ngOnInit()`, we can consume changes with `effect()` or `computed()`.

The second addition is the additional `required()` function. It fixes the issue with `@Input({required: true})`, which always results in the union type including `undefined` and the actual type:

```typescript
// required input
class HolidayComponent {
  username = input(''); // Signal<string>
  holiday = input.required<Holiday>(); // Signal<Holiday>
} 

@Component({
  tempate: `<app-holiday [username]="username" [holiday]="holiday" />`
})
class HolidayContainerComponent {
  username = 'Konrad Weber';
  holiday = createHoliday();
}
```

## Testing Signal Inputs

How do we test that? We can set input properties via the `componentInstance` or the `setInput()` like this:

```typescript
// Input Signal via componentInstance
it('should show username and holiday', () => {
  const fixture = TestBed.configureTestingModule({
    imports: [HolidayComponent],
  }).createComponent(HolidayComponent);

  const holiday = signal(createHoliday({ title: 'London' }));

  fixture.componentInstance.holiday =
    holiday as unknown as typeof fixture.componentInstance.holiday;

  fixture.detectChanges();

  const body: HTMLParagraphElement = fixture.debugElement.query(
    By.css('[data-testid=txt-body]'),
  ).nativeElement;

  expect(body.textContent).toContain('Are you interested in visiting London?');

  holiday.update((value) => ({ ...value, title: 'Vienna' }));
  fixture.detectChanges();

  expect(body.textContent).toContain('Are you interested in visiting Vienna?');
});

// Input Signal via componentRef
it('should show username and holiday', () => {
  const fixture = TestBed.configureTestingModule({
    imports: [HolidayComponent],
  }).createComponent(HolidayComponent);

  fixture.componentRef.setInput('holiday', createHoliday({ title: 'London' }));
  fixture.detectChanges();

  const body: HTMLParagraphElement = fixture.debugElement.query(
    By.css('[data-testid=txt-body]'),
  ).nativeElement;
  expect(body.textContent).toContain('Are you interested in visiting London?');

  fixture.componentRef.setInput('holiday', createHoliday({ title: 'Vienna' }));
  fixture.detectChanges();

  expect(body.textContent).toContain('Are you interested in visiting Vienna?');
});
```

**Try to avoid that!**

There is also another option that goes under different names. I tend to call it the "Wrapper Component" pattern. Another name is the Host Component. We create a Component for the test that uses the `HolidayComponent` and apply Property Binding.

### The "Wrapper Component"/"Host Component" pattern

The test communicates with the "Wrapper Component" and passes over the task of Property Binding to Angular:

```typescript
@Component({
  template: ` <app-holiday [holiday]="holiday" />`,
  standalone: true,
  imports: [HolidayComponent],
})
class HolidayWrapperComponent {
  holiday = createHoliday({ title: 'London' });
}
it('should show username and holiday', () => {
  const fixture = TestBed.configureTestingModule({
    imports: [HolidayWrapperComponent],
  }).createComponent(HolidayWrapperComponent);

  const { componentInstance } = fixture;
  fixture.detectChanges();

  const body: HTMLParagraphElement = fixture.debugElement.query(
    By.css('[data-testid=txt-body]'),
  ).nativeElement;

  expect(body.textContent).toContain('Are you interested in visiting London?');
  componentInstance.holiday = createHoliday({ title: 'Vienna' }); 
  fixture.detectChanges();

  expect(body.textContent).toContain('Are you interested in visiting Vienna?');
});
```

## Why the "Wrapper Component"?

We don't know how Property Binding internally works. By directly setting a property via the `componentInstance`, we take a shortcut and remove Angular's part from the equation.

A common argument for that approach is that we don't want to involve Angular in our tests because we don't want to test Angular.

That's not true. Our code runs with Angular. By removing Angular, we are taking high risks that the test won't reflect the runtime behavior and thus will produce wrong results.

A simple example: You set the property before you run `fixture.detectChanges()`. Is this also the way how Angular does it? Does it set the property in the constructor, in `ngOnInit()`, between the instantiation and `ngOnInit()`? What about `ngOnChanges()` and the `afterNextRender()`, `afterRender()`?

We would have to call all these hooks manually and know when to do that. In that sense, we're re-implementing parts of the Angular framework.

Wouldn't it be better if we just let Angular do its things? We do ours, and the test runs both. Much better!

We should always try to run our tests in an environment as close to the applications as possible. If that doesn't come with additional costs, like slower tests or significantly more code, then do that. 

> Testing is not easy; don't make it harder for yourself as it already is ;).

`ComponentRef::setInput()` should work in most cases as long as you test via the DOM. `setInput()` is part of the Angular API, and the Angular Team designed it for that use case.

When you use Signal Inputs, directly setting properties on the component instance is not possible anymore.

The same also applies to testing the old `@Input()`. Testing tools, like the "[Testing Library](https://testing-library.com/docs/angular-testing-library/intro/)" or "[Cypress Component Test Runner](https://docs.cypress.io/guides/component-testing/angular/overview)", support the "Wrapper Component" pattern out of the box.

## Model Inputs

Where `input()` is one-way, `model()` is two-way binding. As a result, the Signal becomes writable, and we also have the `required()` function available.

The "Wrapper Component" pattern makes testing the `model()` a no-brainer.

Let's say `HolidayComponent` wants to introduce a rating feature for the holiday and emits an event whenever the rating changes. We must replace the `input()` with the `model()`.

The parent component can still apply Property Binding but gets a new event named `holidayChange`. This allows the parent to use the classic two-binding via the "banana box" syntax:

```typescript
@Component({
  selector: 'app-holiday',
  template: `<p data-testid="txt-greeting">Hello {{ username() }}</p>
    <p data-testid="txt-body">
      Are you interested in visiting {{ holiday().title }}?
    </p>
    <p> Rate your Holiday </p>
    <button mat-raised-button data-testid="btn-up" (click)="rating.set('👍')"
      >👍</button
    >
    <button mat-raised-button data-testid="btn-down" (click)="rating.set('👎')"
      >👎</button
    >`,
  standalone: true,
  imports: [MatButton],
})
export class HolidayComponent {
  username = input('');
  holiday = input.required<Holiday>();
  rating = model.required<'👍' | '👎'>();
}
```

Again, we use the "WrapperComponent", which does two-way binding on rating:

```typescript
@Component({
  template: ` <app-holiday [holiday]="holiday" [(rating)]="rating" />
    <p data-testid="txt-rating">{{ rating }}</p>`,
  standalone: true,
  imports: [HolidayComponent],
})

class HolidayWrapperComponent {
  holiday = createHoliday({ title: 'London' });
  rating = '👎';
}

describe('Holiday Component', () => {
  it('should apply two-way-binding on rating', () => {
    const fixture = TestBed.configureTestingModule({
      imports: [HolidayWrapperComponent],
    }).createComponent(HolidayWrapperComponent);
    fixture.detectChanges();
    const rating: HTMLParagraphElement = fixture.debugElement.query(
      By.css('[data-testid=txt-rating]'),
    ).nativeElement;
    expect(rating.textContent).toBe('👎');
    fixture.debugElement
      .query(By.css('[data-testid=btn-up]'))
      .nativeElement.click();
    fixture.detectChanges();
    expect(rating.textContent).toBe('👍');
  });
});
```

`HolidaysWrapperComponent` shows the current rating and passes it on to the `HolidayComponent`. Since we use the "banana box" syntax, any change to the `HolidayComponent`'s rating would also happen to the wrapper's rating.

## Summary

Testing Signal and Model Inputs is very easy as long as we communicate with the Component via the DOM and let Angular the Property Binding.

The "Wrapper Component" pattern or `ComponentRef::setInput()` are the right approaches to that common testing use case.

---

You can access the repository at https://github.com/rainerhahnekamp/how-do-i-test

If you encounter a testing challenge you'd like me to address here, please get in touch with me!

For additional updates, connect with me on [LinkedIn](https://www.linkedin.com/in/rainerhahnekamp), [X](https://twitter.com/rainerhahnekamp), and explore our website for workshops and consulting services on testing.

{% embed https://www.angulararchitects.io/en/training/professional-angular-testing-playwright-edition/ %}