The landscape of Angular testing is undergoing a significant transformation as the year concludes, with Angular version 21 poised to introduce a series of groundbreaking changes. While discussions around features like Signal Forms have taken center stage, a quiet revolution in how Angular applications are tested is imminent. This article will explore the pivotal role of Vitest as the new default testing framework, the implications of zoneless applications on asynchronous testing, and the emergence of Testronaut, a community-driven solution for Playwright Component Testing in Angular.

Vitest: The New Horizon for Angular Testing

For nearly two years, Angular developers faced uncertainty regarding their testing strategy following the deprecation of Karma in 2023. The absence of a clear successor left many teams deliberating between holding onto older tools, adopting Jest, or considering Vitest. This period of ambiguity made future planning, especially for new projects, particularly challenging.

The recent announcement that Vitest will become the default testing framework starting with Angular 21 brings much-needed clarity and relief. This decision provides developers with a definitive path forward, eliminating the previous indecision.

Vitest emerges as the right choice due to its robust API and a modern, forward-thinking ecosystem. It boasts first-class TypeScript support and full compatibility with ESM. A critical factor for the Angular team was Vitest’s browser mode, which allows tests to run in a real browser environment, mirroring the capabilities of Jasmine/Karma without forcing a complete rewrite of existing test environments. Although API differences exist, particularly in async testing and mocking, the underlying execution environment remains consistent.

Powered by Vite, Vitest also helps bridge Angular with the broader JavaScript ecosystem, aligning Angular more closely with modern development practices, even if Angular’s implementation doesn’t fully embrace Vite for test building.

Vitest offers a powerful set of features, including:

  • Polling Feature (expect.poll): This allows assertions to continuously execute until a condition is met or a timeout occurs, ideal for handling asynchronous operations.
    import { expect, test } from 'vitest';
    
    test('wait for the asynchronous tasks to end', async () => {
      let a = 1;
    
      setTimeout(() => a++);
      Promise.resolve().then(() => a++);
    
      await expect.poll(() => a).toBe(3);
    });
    
  • Soft Assertions (expect.soft): Unlike standard assertions that halt execution on the first failure, soft assertions continue to run all checks within a test, marking the test as failed if any assertion fails.
    test('resource', () => {
      const todoResource = getSomeResource();
    
      expect.soft(resource.status()).toBe('resolved');
      expect.soft(resource.error()).toBeUndefined();
      expect.soft(resource.hasValue()).toBe(true);
    })
    
  • Test Context: Vitest’s test context enables the injection of additional functionalities into tests, similar to dependency injection. This allows for cleaner, more organized test code.
    // context.ts
    import { test as base } from 'vitest';
    
    interface Fixtures {
      wait: (timeout?: number) => Promise<void>;
    }
    
    export const test = base.extend<Fixtures>({
      wait: async ({}, use) => {
        await use(
          (timeout = 0) =>
            new Promise<void>((resolve) => setTimeout(resolve, timeout)),
        );
      },
    });
    
    // async.spec.ts
    import { test } from './context';
    import { expect } from 'vitest';
    
    test('wait for the asynchronous task', async ({ wait }) => {
      let a = 1;
    
      setTimeout(() => a++);
      Promise.resolve().then(() => a++);
    
      expect(a).toBe(1);
      await wait();
      expect(a).toBe(3);
    });
    

Vitest is already usable in Angular 20, allowing developers to run ng test as usual. With Angular 21, ng new will offer Vitest as the default choice over Jasmine, and migration schematics will be available to facilitate the transition. For more information, developers can refer to Matthieu Riegler’s LinkedIn post and the official Vitest website: https://vitest.dev.

The Demise of waitForAsync() & fakeAsync() in Zoneless Angular

Zone.js has historically been integral to Angular’s management of asynchronous operations, patching timing functions like setTimeout and setInterval to track async tasks. However, it could not patch native JavaScript Promises, which are language-level constructs. This led to the creation of fakeAsync() and waitForAsync() for Angular testing.

  • waitForAsync(): This function would wait for all scheduled asynchronous tasks to complete before proceeding with the test. While functional for many cases, it struggled with long-running timers or assertions placed after async tasks.
    typescript
    it('should wait for async tasks to end', waitForAsync(() => {
    let a = 1;
    setTimeout(() => {
    a++;
    expect(a).toBe(2);
    });
    }));
  • fakeAsync(): Offering more granular control, fakeAsync() allowed developers to manage the async queue with tick() (simulating time passage) and flush() (executing all pending timers).
    typescript
    it('runs all asynchronous tasks immediately (synchronously) after flush', fakeAsync(() => {
    let a = 1;
    setTimeout(() => { a++; }, 5_000);
    setTimeout(() => { a++; }, 5_000);
    flush();
    expect(a).toBe(3);
    }));

With Angular 20.2’s stable zoneless mode and its default status in Angular 21, fakeAsync() and waitForAsync() will cease to function unless Zone.js is explicitly enabled. This necessitates rewriting portions of existing tests that rely on these utilities.

Modern Async Control with Vitest (and Jest) Fake Timers

Thankfully, modern testing frameworks provide robust alternatives in the form of fake timers. These utilities mock async APIs like setTimeout and setInterval, offering similar control to the Zone.js-based functions but with key differences.

Key fake timer functions include:

  • runAllTimers(): Executes all asynchronous tasks, including periodic ones, and covers tasks triggered by other async tasks. This extends the functionality of flush().
  • advanceTimersByTime(ms): Progresses time by a specified duration, akin to tick() in fakeAsync().
  • runOnlyPendingTimers(): Executes only currently scheduled timers.

Fake timers must be explicitly enabled and then reset in beforeEach and afterEach hooks, respectively.

describe('async tasks', () => {
  beforeEach(() => {
    vitest.useFakeTimers();
  });

  afterEach(() => {
    vitest.resetAllMocks();
  });

  it('runs all asynchronous tasks immediately (synchronously)', () => {
    let a = 1;
    setTimeout(() => { a++; }, 5_000);
    setTimeout(() => { a++; }, 10_000);
    vitest.runAllTimers();
    expect(a).toBe(3);
  });
});

A crucial distinction lies in handling Promises. Unlike Zone.js, fake timers do not compile native Promises, meaning they cannot directly mock them. This can lead to issues where Promises remain unresolved by standard fake timer calls.

To address this, Vitest provides async versions of its fake timer functions: runAllTimersAsync() and advanceTimersByTimeAsync(). These functions queue an additional Promise after the real one, ensuring all asynchronous behavior, including native Promises, completes.

describe('async tasks', () => {
  beforeEach(() => {
    vitest.useFakeTimers();
  });

  afterEach(() => {
    vitest.resetAllMocks();
  });

  it('succeeds on Promises', async () => {
    let a = 1;
    Promise.resolve().then(() => a++);
    await vitest.runAllTimersAsync();
    expect(a).toBe(2);
  });
});

Even when Zone.js is still in use, adopting these fake timers for new tests is the recommended approach.

Testronaut: Empowering Playwright Component Testing for Angular

While unit and integration tests are vital, End-to-End (E2E) tests offer unparalleled realism and coverage. E2E frameworks automatically handle asynchronous behavior and simulate genuine user interactions, ensuring elements are not just present but also usable. However, the requirement to run the entire application for E2E tests can make reaching and testing specific features slow and cumbersome.

Cypress Component Testing (CT) previously offered a superior approach by compiling and mounting individual Angular components in the browser, allowing for focused E2E-like testing at the component level. Today, Playwright stands as the preferred E2E framework, but its component testing capabilities traditionally required full Vite support, posing challenges for Angular.

Despite significant community efforts to integrate Playwright CT with Angular via Vite, the Playwright team ultimately closed the relevant pull request in November 2023, citing doubts about the long-term viability of CT and a desire to reduce framework-specific maintenance.

Out of this setback, Testronaut was born. Led by Younes Jaaidi (a key contributor to both the original Playwright CT for Angular effort and Cypress CT for Angular), Testronaut is a community-driven Playwright Component Testing runner specifically designed for Angular. Crucially, it bypasses the need for Angular to compile via Vite, instead leveraging the Angular CLI. This ensures developers benefit from the familiar build setup and speed of ng serve without needing awkward workarounds.

Testronaut mounts components, allowing Playwright to execute comprehensive, browser-based tests with the realism and async handling expected from a full E2E framework, but with a component-centric focus. The test code is intuitive and straightforward:

test('should emit an event on click', async ({ mount, page }) => {
  await mount(ClickMeComponent, { inputs: { clickMeLabel: 'Press me' } });
  await page.getByRole('button', { name: 'Press me' }).click();
  await expect(page.getByText('Lift Off!')).toBeVisible();
});

Testronaut is currently available, though it’s advised to wait for the release of migration schematics (ng add @testronaut/angular) for the smoothest integration experience. More details can be found at https://testronaut.dev.

These advancements mark a significant evolution in Angular’s testing ecosystem, promising clearer directions, more robust tools, and a more streamlined testing workflow for developers.

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.
You need to agree with the terms to proceed