Testing Angular lifecycle hooks

January 1st 2021 Angular Unit Testing

There's no specific guidance for testing Angular lifecycle hooks in the component testing scenarios covered by the Angular documentation. Maybe because they can be tested like any other method: a test can set up the component state and then manually invoke the hook.

However, some caution is needed since hooks can also be called implicitly by Angular. This can affect test results. Let's use an almost trivial hook to take a closer look at that:

ngAfterViewInit(): void {
  this.sampleService.sampleMethod(this.sampleValue);
}

When invoking the hook from a test only once, two calls will be recorded:

it("should include one from implicit ngAfterViewInit invocation", () => {
  component.ngAfterViewInit();

  expect(sampleServiceMock.sampleMethod).toHaveBeenCalledTimes(2);
});

That's because one call will be recorded even when the hook is not explicitly invoked from a test:

it("should include one even without explicit ngAfterViewInit call", () => {
  expect(sampleServiceMock.sampleMethod).toHaveBeenCalledTimes(1);
});

If you're checking the call arguments, the ones from the implicit call might be different than expected:

it("should include args from implicit ngAfterViewInit invocation", () => {
  component.sampleValue = true;
  component.ngAfterViewInit();

  expect(sampleServiceMock.sampleMethod).toHaveBeenCalledWith(false);
});

The test will also succeed if you check for the arguments supplied in the explicit call:

it("should include args from explicit ngAfterViewInit call", () => {
  component.sampleValue = true;
  component.ngAfterViewInit();

  expect(sampleServiceMock.sampleMethod).toHaveBeenCalledWith(true);
});

That's because the toHaveBeenCallesWith() method succeeds as soon as there was one call matching the arguments. To make the test reliable, you have to check the arguments of the most recent call. They won't match the implicit call but will match the latest explicit call:

it("most recent args should not match implicit ngAfterViewInit invocation", () => {
  component.sampleValue = true;
  component.ngAfterViewInit();

  expect(sampleServiceMock.sampleMethod.calls.mostRecent().args).not.toEqual([
    false,
  ]);
});

it("most recent should include args from explicit ngAfterViewInit call", () => {
  component.sampleValue = true;
  component.ngAfterViewInit();

  expect(sampleServiceMock.sampleMethod.calls.mostRecent().args).toEqual([
    true,
  ]);
});

Unfortunately, this makes the test expectation a bit more difficult to read and understand. A better solution is to reset any recorded calls before doing the explicit calls you want to test. This allows you to use the simpler expectation syntax again:

it("should only include args from explicit ngAfterViewInit call after reset", () => {
  sampleServiceMock.sampleMethod.calls.reset();

  component.sampleValue = true;
  component.ngAfterViewInit();

  expect(sampleServiceMock.sampleMethod).toHaveBeenCalledOnceWith(true);
});

It also gives expected results when only validating the call count:

it("should only include args from explicit ngAfterViewInit call after reset", () => {
  sampleServiceMock.sampleMethod.calls.reset();

  component.sampleValue = true;
  component.ngAfterViewInit();

  expect(sampleServiceMock.sampleMethod).toHaveBeenCalledOnceWith(true);
});

You can run all these tests yourself from a sample project in my GitHub repository.

When testing Angular lifecycle hooks, you can manually call them from your tests. If you do that, it's best to reset the mocked method before you call the hook. This way you can be sure that you're not accidentally testing the effects of previous implicit hook invocations by Angular runtime.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License