Testing

Introduction to Vitest and Angular

The Angular team deprecated Karma a few versions ago and are currently working on ways to provide an alternative 3rd party unit testing frameworks. Currently the options talked about so far are Web Test Runner (likely to be the default), this is a browser based unit test runner similar in many ways

Introduction to Vitest and Angular — Testing article by Duncan Faulkner on Angular In Depth
Introduction to Vitest and Angular — Testing article by Duncan Faulkner on Angular In Depth
On this page · 1 sections

For a few releases now, the Angular team has marked Karma as deprecated, shifting focus toward supporting alternative third-party unit testing frameworks. The primary candidates under discussion include Web Test Runner, which is likely to become the default choice—a browser-based runner that bears many similarities to Karma. Alongside it, Jest has been proposed, and while it can be installed today, configuration can occasionally be finicky, especially when Angular projects are set up with ES builds, which has been the default since Angular 18.

The Angular team envisions integrating these test runners directly into the CLI installation flow, a bit like how you pick a CSS framework when scaffolding a new project. However, this work is still in its nascent stages, far from a developer preview (currently labelled experimental), and there’s no word yet on how it might apply to existing applications.

Thanks to Angular's move to Vite as the development build server, standard since Angular 18, Vitest is now a viable option for our Angular projects. The setup is straightforward and, based on my recent attempts, feels less troublesome than configuring Jest. The best part is that Vitest’s syntax closely mirrors both Karma and Jest, so the transition should be relatively smooth.

For further details on migrating Angular's build system, refer to the official documentation: Angular build system migration

In this guide, I'll demonstrate how to install and configure Vitest in an Angular 18 application, replacing Karma. I won't dive into writing unit tests now—saving that for a separate article.

Although I'll create a fresh project for this demonstration, the steps should work equally well on an already established codebase.

First, open a terminal in your desired directory and run:

ng new my-vitest-app  && cd my-vitest-app && npm i

This command scaffolds a new Angular app named my-vitest-app, navigates into that folder, and then executes npm install. With the application ready, we can proceed to remove Karma.

From the terminal, execute:

npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter

There’s no need to remove @types/jasmine or jasmine-core, since Vitest works with Jasmine underneath. This allows us to use it and describe directly from Jasmine without any import statements in our test files. Although Vitest provides its own it and describe functions, opting for them would prevent us from using utilities such as fakeAysnc in our tests.

The simplest route to integrate Vitest into our project is through a plugin, and AnalogJS offers exactly the tool we need to handle the setup and configuration.

In the terminal, type:

npm i @analogjs/platform -D

Then follow it with:

ng g @analogjs/platform:setup-vitest --project my-vitest-app		

This package is responsible for installing the subsequent files:

 "devDependencies": {
     // other files removed for brevity 
    "@analogjs/platform": "^1.9.0",
    "@analogjs/vite-plugin-angular": "^1.9.0",
    "@analogjs/vitest-angular": "^1.9.0",
    "@nx/vite": "~19.8.2",
    "@vitest/coverage-v8": "^2.1.3",
    "@vitest/ui": "^2.1.3",
    "vite": "^5.4.9",
    "vite-tsconfig-paths": "^4.2.0",
    "vitest": "^2.1.3"
  }

As of this writing, the @analogjs library pulls in @nx/vite version 19.8.2, though Brandon Roberts (the creator of AnalogJS) has issued a beta release that supports @nx/vite 20.0.3.

The @analog package should set up version 2.1.3 for vitest, @vitest/ui, and @vitest/coverage-v8. If it happens to install 1.6.0 instead, you’ll need to uninstall those packages and reinstall them with:

npm i @vitest/coverage-v8 @vitest/ui vitest@latest -D

Once that’s done, the following files will be generated:

// vite.config.mts
/// <reference types="vitest" />
import angular from '@analogjs/vite-plugin-angular';
import { defineConfig } from 'vite';

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  return {
    plugins: [
      angular(),
    ],
    test: {
      globals: true,
      environment: 'jsdom',
      setupFiles: ['src/test-setup.ts'],
      include: ['**/*.spec.ts'],
      reporters: ['default'],
    },
    define: {
      'import.meta.vitest': mode !== 'production',
    },
  };
});

The vite.config.mts file, located at the project root, configures Vitest—including the environment, which defaults to jsdom but can be switched to happy-dom (after installing happy-dom). Meanwhile, the test-setup.ts file sits in the src directory and establishes the test environment.

import '@analogjs/vitest-angular/setup-zone';
import {
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
import { getTestBed } from '@angular/core/testing';

getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);

For those using azoneless application, the test-setup.ts will look a bit different:

import '@analogjs/vitest-angular/setup-snapshots';

import {
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
import { getTestBed } from '@angular/core/testing';

getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);

Within angular.json, the test block will be swapped out for:

 "test": {
    "builder": "@analogjs/vitest-angular:test"
 }

Finally, the tsconfig.spec.json will be modified to:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/spec",
    "types": [
      "jasmine",
      "vitest/globals" // added
    ],
    "target": "es2016" // added
  },
  "include": [
    "src/**/*.spec.ts",
    "src/**/*.d.ts"
  ],
  "files": [
    "src/test-setup.ts" // added
  ]
}

To execute Vitest tests, we need to update the scripts section by adding the following to our application’s package.json:

{
 // other scripts
 "test" : "vitest"   
}

// we could also use this, if you are not the running vitest command
{
    "test" : "ng test --watch"
}

Running npm run test will execute all test files in our project.

If you'd rather not install Volta, feel free to skip ahead.

To invoke Vitest directly from the terminal, we first need to install it. There are a couple of options: a global installation via npm i -g vitest, or using Volta—a toolchain manager akin to Node Version Manager (NVM), but with support for more than just Node.

If you already have a version of node installed, you'll need to uninstall it before installing Volta; afterwards, you can have multiple node versions installed without removing prior ones.

Head to https://volta.sh/ to download the installer suited to your operating system. As an Ubuntu user, the process is as simple as:

One of Volta's handy features is pinning the node version to your package.json, ensuring Volta always selects the correct Node version—especially useful in team settings. To pin a version, run volta pin node@20.15.0.

With volta and node set up, we can proceed to install Vitest:

volta install vitest

After Vitest is installed, volta ls will display a list of all the tooling we have installed.

# example listing from Volta
⚡️ Currently active tools:

    Node: v20.15.1 (default)
    Tool binaries available:
        vitest (current @ /home/repo/vitest-app/package.json)

With everything in place, we can now run our unit tests. To target a subset of tests (or a single test), we’ll use the terminal and pass an extra parameter—this will run any test file that contains the given pattern.

# run a range of unit tests
vitest dashboard

Alternatively, we can specify a file directly; this will only run tests matching that filename, though it doesn't support regex or glob patterns.

# run a specific unit test
vitest app.component

The command above runs tests in watch mode. To execute them just once without watching, modify the command to:

# run unit test(s) once then stop
vitest run [optionally specify an additional parameter]

For CI environments, watch mode is unnecessary, so we can add a test:ci script to the package.json.

{
 // other scripts
 "test" : "vitest",
 "test:ci" : "vitest run"
}

Of course, we’ll want our build pipelines to run these tests; setting up those pipelines is beyond this article’s scope, but plenty of resources cover it.

Next, let’s discuss code coverage. After writing tests for various features, the question arises: have we written enough? Did we cover all crucial paths? Code coverage helps answer this by measuring how much of our code is exercised by tests, highlighting missed branches—like testing an if condition but not its else counterpart.

Let's add another script to the package.json scripts section.

{
// other scripts    
"coverage":"vitest run --coverage"
}

Running npm run coverage locally will generate a coverage directory at the project root, presenting a table of files alongside their coverage percentages:

File% Stmts% Branch% Funcs% LinesUncovered Line #s
All files100100100100
app.component.ts100100100100

During execution, it may prompt you to install a supporting package, which could be coverage-v8 (the default) or coverage-instanbul. If not, you can install them manually:

# For v8
npm i -D @vitest/coverage-v8

# For istanbul
npm i -D @vitest/coverage-istanbul

The provider type can be specified in the coverage section of the vite.config.mts file:

export default defineConfig(({ mode }) => {
  return {
    plugins: [angular()],
    test: {
      globals: true,
      environment: "jsdom",
      setupFiles: ["src/test-setup.ts"],
      include: ["**/*.spec.ts"],
      reporters: ["default"],
      coverage: {
        provider: "v8",
        reporter: ["text", "json", "html"],
      },
    },
    define: {
      "import.meta.vitest": mode !== "production",
    },
  };
});

For code coverage in CI, we should modify the test:ci script to include a reporter. Here, I've opted for junit with the output file junit.xml. Azure DevOps offers a convenient plugin that reads junit.xml files, turning them into a polished dashboard.

script:{
// other scripts
"test:ci": "vitest run --reporter=default --reporter=junit --outputFile=reports/junit.xml",
}

In Azure devops, we can add the PublishTestResults task to our build pipelines, scheduling it to run after the unit tests have finished.

By default, test results appear in the terminal (unless in CI), but those preferring a visual interface can install the UI tooling:

npm i @vitest/ui

Be sure to align the version of @vitest/ui with your installed vitest version. Once installed, kick it off with vitest --ui from the terminal. Additionally, adding html to the reporters array in vite.config.ts will generate an html directory containing all the relevant files.

export default defineConfig(({ mode }) => {
  return {
    plugins: [angular()],
    test: {
      globals: true,
      environment: "jsdom",
      setupFiles: ["src/test-setup.ts"],
      include: ["**/*.spec.ts"],
      reporters: ["default", "html"], // add html into this array.
      coverage: {
        provider: "v8",
        reporter: ["text", "json", "html"],
      },
    },
    define: {
      "import.meta.vitest": mode !== "production",
    },
  };
});

Conclusion

We’ve covered the installation and configuration of Vitest, a Vite-based unit test runner for Angular, along with integrating code coverage to gauge test thoroughness. We also explored how to set this up in Azure DevOps CI pipelines.

For those interested in a live example, I've shared a repository on GitHub: Vitest test application.


Introduction to Vitest and Angular — figure 1

Tagged in:

Articles

Last Update: October 24, 2024

DF
Duncan Faulkner

Writes about Signals, Testing. Active 2024–2026.

All 2 articles →