> ## Content Index
> Fetch the complete content index at: https://www.angularspace.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Angular Zoneless Unit Testing
- URL: https://www.angularspace.com/angular-zoneless-unit-testing/
- Published: 2025-09-30T06:30:10.000Z
- Updated: 2025-09-30T06:30:37.000Z
- Author: Francesco Borzì
- Tags: Articles

[](https://medium.com/plans?dimension=post%5Faudio%5Fbutton&postId=e048e1d9220d&source=upgrade%5Fmembership---post%5Faudio%5Fbutton-----------------------------------------)

![](https://miro.medium.com/v2/resize:fit:700/1*PofLTJ-RQSaxJymmIgQHxw.jpeg)

## The Future Is Zoneless — What Can We Do Today?

You’ve probably heard: Angular is moving toward a [**Zoneless**](https://angular.dev/guide/zoneless?ref=angularspace.com) 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**](https://angular.dev/guide/signals?ref=angularspace.com).

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*](https://angularexperts.ch/blog/zoneless-angular?ref=angularspace.com) *for general tips on gradually adapting your application code to Zoneless.*

## Enabling Zoneless In Your Tests

First of all, add [provideZonelessChangeDetection()](https://angular.dev/api/core/provideZonelessChangeDetection?ref=angularspace.com) to the list of your providers:

```typescript
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](https://angular.dev/guide/zoneless?ref=angularspace.com#using-zoneless-in-testbed) 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:

```typescript
// 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:

```typescript
// 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](https://medium.com/javascript-in-plain-english/dealing-with-asynchronous-behavior-in-angular-tests-async-await-whenstable-vs-fakeasync-2999aef75fbf?ref=angularspace.com), these helpers have been extremely popular in the Angular testing world, so chances are you have been using them in your project.

```typescript
// 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](https://francescoborzi.github.io/ngx-page-object-model/?ref=angularspace.com), or just copy it from [here](https://github.com/FrancescoBorzi/ngx-page-object-model/blob/main/libs/ngx-page-object-model/src/lib/tick-async.ts?ref=angularspace.com). Example usage:

```typescript
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:

```typescript
// ⚠️ 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](https://github.com/angular/angular/issues/55295?ref=angularspace.com) for more information), it is worth [mentioning an example of using mock clocks in Jasmine](https://github.com/angular/angular/blob/3a320bdec996673abbec8646bb417bd3618030af/adev/src/app/editor/code-editor/code-mirror-editor.service.spec.ts?ref=angularspace.com#L146-L156):

```typescript
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*](https://github.com/jasmine/jasmine/pull/2042?ref=angularspace.com) *to the Jasmine library from* [*Andrew Scott*](https://github.com/atscott?ref=angularspace.com) *who has been heavily involved in the support for Zoneless. Special thanks to* [*Matthieu Riegler*](https://github.com/JeanMeche?ref=angularspace.com) *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:

```typescript
// 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:

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

![](https://miro.medium.com/v2/resize:fit:700/1*zc3NjCxOOF7E1cMRhk-wSA.jpeg)

Mount Etna — Photo by [Nienke Koedijk](https://nienkekoedijk.com/?ref=angularspace.com)

## 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

---

![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/09/Screenshot-2024-11-13-at-15--1---1---2-.jpg)