Sitemap
JavaScript in Plain English

New JavaScript and Web Development content every day. Follow to join our 3.5M+ monthly readers.

Follow publication

Angular Zoneless Unit Testing

--

Press enter or click to view image in full size

The Future Is Zoneless — What Can We Do Today?

You’ve probably heard: Angular is moving toward a Zoneless future. Migrating your Angular app to run without Zone.js brings several benefits, but for medium-to-large applications, the process might not be trivial.

The good news is that you can migrate an Angular application to Zoneless gradually. For example, migrating your components to use OnPush change detection is a recommended step toward Zoneless compatibility — and it also delivers immediate performance benefits. Nowadays, this has become much simpler when using Signals.

Another powerful step is adapting your unit tests to run in Zoneless mode, ensuring that the components under test are compatible with a Zoneless application — even before your app fully runs without zone.js.

While migrating unit tests will be our focus here, I recommend reading this article from Angular Experts for general tips on gradually adapting your application code to Zoneless.

Enabling Zoneless In Your Tests

First of all, add provideZonelessChangeDetection() to the list of your providers:

TestBed.configureTestingModule({
providers: [
provideZonelessChangeDetection(),
// ...
]
});

It’s a good idea to enforce this for new component tests while gradually migrating existing ones. Depending on how your current components and tests are structured, you may encounter some failures once you enable it.

Avoid Calling detectChanges() — especially more than once

The Angular documentation specifically recommends against manually calling fixture.detectChanges() in your test code:

To ensure tests have the most similar behavior to production code, avoid using fixture.detectChanges() when possible. This forces change detection to run when Angular might otherwise have not scheduled change detection.

Instead, await fixture.whenStable() should be used:

// not recommended (still ok)
it('should do something', () => {
const { page } = setup();

page.triggerSomeAction();
page.fixture.detectChanges();

expect(something).toBe(true);
});

// recommended
it('should do something', async () => {
const { page } = setup();

await page.fixture.whenStable();

expect(something).toBe(true);
});

I noted “still ok” in the comment above detectChanges() since:

For existing test suites, using fixture.detectChanges() is a common pattern and it is likely not worth the effort of converting these to await fixture.whenStable(). TestBed will still enforce that the fixture's component is OnPush compatible and throws ExpressionChangedAfterItHasBeenCheckedError if it finds that template values were updated without a change notification

However, I noticed that calling detectChanges() more than once often causes issues when used with OnPush and/or Zoneless:

// usually problematic - avoid!
it('should correctly react on action 1 and action 2', () => {
const { page } = setup();

page.triggerActionOne();
page.fixture.detectChanges(); // first call to detectChanges()
expect(something).toBe(true);

page.triggerActionTwo();
page.fixture.detectChanges(); // second call, often problematic!
expect(something).toBe(true);
});

// do this instead - keep each action separate!
it('should correctly react on action 1', async () => {
const { page } = setup();

page.triggerActionOne();
await page.fixture.whenStable();

expect(something).toBe(true);
});

it('should correctly react on action 2', async () => {
const { page } = setup();

page.triggerActionTwo();
await page.fixture.whenStable();

expect(something).toBe(true);
});

Get rid of fakeAsync() and tick()

Yes, I was surprised too when I realized this. Unfortunately fakeAsync()and tick() rely on Zone.js and will no longer work once it’s completely removed as a dependency in your tests.

As I mentioned in another article, these helpers have been extremely popular in the Angular testing world, so chances are you have been using them in your project.

// will not work without the zone.js dependency
it('should do something async', fakeAsync(() => {
const { page } = setup();

page.doSomething();
tick();

expect(something).toBe(true);
}));

Note that you can keep using fakeAsync() and tick() even with provideZonelessChangeDetection() enabled as long as zone.js is included as a dependency in your unit tests. So this step can be postponed.

Alternatives to fakeAsync() and tick()

At the time of writing this article, the Angular team is working with testing library tools such as Jasmine and Jest to provide a proper alternative. They will likely update the Angular docs will new recommendations in the near future. Meanwhile, the recommended route is using await fixture.whenStable() instead.

However, while awaiting whenStable() is indeed the recommended approach when it works, in my experience there are two cases where it may not be suitable:

  • When it’s not available, because you’re testing something other than a Component (e.g. a Service)
  • When, for some reason, it doesn’t actually wait for the desired action to complete

To address this, I wrote a small utility called tickAsync(). The implementation is very basic, you can find it in the lightweight testing library ngx-page-object-model, or just copy it from here. Example usage:

import { tickAsync } from 'ngx-page-object-model'; // or copy it from GitHub

it('should do something async', async () => {
const { page } = setup();

page.doSomething();
// use this instead of tick() whenever fixture.whenStable() cannot be used
await tickAsync();

expect(something).toBe(true);
});

With this I was able to fix most of the tests where whenStable() couldn’t help. Only in a few rare cases I had to also manually wait with a delay:

// ⚠️ it will ACTUALLY wait for 100ms - not ideal.
await tickAsync(100);

This is however, not ideal. Tests should not await real-time.

The best alternative for fake timers are actually provided by the testing framework such as Jasmine or Jest. While we wait for an official recommendation from Angular (you can follow this GitHub issue for more information), it is worth mentioning an example of using mock clocks in Jasmine:

it('should write the changed file content to the sandbox filesystem', () => {
jasmine.clock().install();
jasmine.clock().mockDate();
const newContent = 'new content';

const nodeRuntimeSandboxSpy = spyOn(fakeNodeRuntimeSandbox, 'writeFile');

dispatchDocumentChange(newContent);
jasmine.clock().tick(EDITOR_CONTENT_CHANGE_DELAY_MILLIES);

expect(nodeRuntimeSandboxSpy).toHaveBeenCalledWith(service.currentFile().filename, newContent);
jasmine.clock().uninstall();
});

Also worth mentioning this PR to the Jasmine library from Andrew Scott who has been heavily involved in the support for Zoneless. Special thanks to Matthieu Riegler for suggesting these examples.

So using the mock clocks provided by the testing library is the best way to replace tick(). Different testing libraries such as Jest have similar APIs and describing all of them goes beyond the scope of this article.

Remove zone.js dependency entirely from your tests

Once you have reached the point where nothing in your tests rely on zone.js, you can remove it entirely as a dependency of your tests.

Remove this from your “test” target in project.json (NX) or angular.json (Angular CLI) file:

// remove this line
"polyfills": ["zone.js", "zone.js/testing"],

Or, if your project has a test.ts setup file, make sure it no longer imports zone.js:

// delete these lines
import 'zone.js';
import 'zone.js/testing';

Conclusions

  • Angular is going to be Zoneless, this will bring several benefits
  • Migrating to Zoneless is usually a complex process that can be done gradually
  • Using OnPush and Signals paves the way to Zoneless compatibility
  • Unit Tests can help you check whether your components work in a Zoneless app even before you fully switch your app to Zoneless
  • You can enable Zoneless mode selectively for individual unit tests
  • Avoid calling fixture.detectChanges() in your tests — especially multiple times. Prefer await fixture.whenStable() instead
  • Avoid using fakeAsync() and tick() as they cannot be used without Zone.js — use mock clocks instead

A message from our Founder

Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.

Did you know that our team run these publications as a volunteer effort to over 200k supporters? We do not get paid by Medium!

If you want to show some love, please take a moment to follow me on LinkedIn, TikTok and Instagram. And before you go, don’t forget to clap and follow the writer️!

--

--

JavaScript in Plain English
JavaScript in Plain English

Published in JavaScript in Plain English

New JavaScript and Web Development content every day. Follow to join our 3.5M+ monthly readers.

Francesco Borzì
Francesco Borzì

Written by Francesco Borzì

Software Architect, loving programming and open source

No responses yet