Rainer Hahnekamp

How do I test using the RouterTestingHarness?

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 Lars Nielsen, have helped us out.

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:

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:

@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:

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".

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:

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:

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, X, and explore our website for workshops and consulting services on testing.

https://www.angulararchitects.io/en/training/professional-angular-testing-playwright-edition/