How do I test using the RouterTestingHarness?
Tags: testing, angular, webdev, programming
Published: 2024-03-06 16:00:00


Historically, creating a test involving the routing context without mocking it has been challenging. This is mainly due to less-than-optimal documentation on `RouterTestingModule` or `provideLocationMock`.

In the meantime, some community projects, especially Spectacular from @layzee, have helped us out.

{% embed https://ngworker.github.io/ngworker/ %}

This article explains how to use the `RouterTestingHarness` to write sound tests where we don't have to mock anything from the Router.

If you are more of a visual learner, here's a video for you:

{% embed https://youtu.be/DsOxW9TKroo %}

In Angular 16.2, we could fetch router parameters via the `@Input`. With 17.1, we have Signal Inputs as an alternative in the form of the `input` function.

Still, the question persists: How do you test it without mocking too much?

The answer has already been available since Angular 15.2. It introduced the `RouterTestingHarness`. It makes testing with minimal mocking a breeze.

This is the component we want to test:

```typescript
@Component({
  selector: 'app-detail',
  template: `<p>Current Id: {{ id() }}</p>`,
  standalone: true,
})
export class DetailComponent {
  id = signal(0);

  constructor() {
    inject(ActivatedRoute)
      .paramMap.pipe(takeUntilDestroyed())
      .subscribe((paramMap) => 
        this.id.set(Number(paramMap.get('id') || '0')));
  }
}
```


The `RouterTestingHarness` replaces the typical `TestBed::createComponent` pattern. It creates the component but wraps it inside a "testing routing context".

Harnesses are pretty popular in Angular Material. They are testing helper classes that make testing very convenient by managing parts of asynchronous execution and triggering the change detection.

There is one requirement, though. Harness commands are asynchronous. So we always end up with async/await tests.

For every routing, we require a configuration. It is the same here:


```typescript
describe('Detail Component', () => {
  it('should test verify the id is 5', waitForAsync(async () => {
    TestBed.configureTestingModule({
      providers: [
        provideRouter([{ path: 'detail/:id', component: DetailComponent }]),
      ],
    });
 }));
});
```

Next, we instantiate the `RouterTestingHarness` and use it to navigate to "/detail/5".

```typescript
const harness = await RouterTestingHarness.create('detail/5');
```

Internally, our component's subscription to the route runs, and we should already see that the `id` in the template shows the value 5:

```typescript
const p: HTMLParagraphElement = harness.fixture.debugElement.query(
  By.css('p'),
).nativeElement;

expect(p.textContent).toBe('Current Id: 5');
```

We can now even extend our test. For example, we might want to stay at the same route but want to switch to a different `id`.

No problem with the `RouterTestingHarness`. For the sake of completeness, here's the full code of the test:

```typescript
describe('Detail Component', () => {
  it('should test verify the id is 5', waitForAsync(async () => {
    TestBed.configureTestingModule({
      providers: [
        provideRouter([{ path: 'detail/:id', component: DetailComponent }]),
      ],
    });

    const harness = await RouterTestingHarness.create('detail/5');

    const p: HTMLParagraphElement = harness.fixture.debugElement.query(
      By.css('p'),
    ).nativeElement;

    expect(p.textContent).toBe('Current Id: 5');

    await harness.navigateByUrl('detail/6');
    expect(p.textContent).toBe('Current Id: 6');
  }));
});
```

Please note that we didn't have to trigger the change detection or do anything else when we switched to "/detail/6". The Harness did everything for us internally. The only thing which we must not forget is to use the `await`.

Testing with the `RouterTestingHarness` is much better than mocking the `ActivatedRouter`.

Whenever you mock functions outside your control, like `ActivatedRouter`, you can never be sure that your mocking behaves precisely as the original.

The original could run asynchronous tasks; it could trigger the change detection or something else internally, which you might not be aware of. There are quite a lot of traps you might fall into.

You better be safe and leave the internal functions to the internal functions 😀.

---

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/ %}