Quickstart: Angular 19 Testing with Jest and Neovim

Quickstart: Angular 19 Testing with Jest and Neovim

Looking to simplify testing in your Angular 19 monorepo and integrate it with Neovim? This guide will show you how to set up Jest, configure Angular for Jest, and use Neotest with the neotest-jest adapter to run tests directly in your editor.

Demo & Source Code

Here is the source code for the sample app used in this guide: GitHub Repo.

Introduction

With this approach, you’ll gain a modern testing setup and an efficient, editor-integrated workflow.

Prerequisites

  1. Angular 19 Monorepo: Ensure your Angular application and libraries are organized in a monorepo. If not, use npm init @angular -- --no-create-application to set one up. More parameters can be found on the Angular CLI documentation page.
  2. Node.js & npm (or yarn/pnpm): Ensure you have a recent version installed.
  3. Neovim: Ensure you’re using a version that supports Lua-based configurations (≥ 0.8 recommended). If you’re using LazyVim, it provides an excellent starting point for modern Neovim setups.

Removing Karma and Jasmine

Uninstall

First, remove all Karma- or Jasmine-related packages from your monorepo:

npm uninstall @types/jasmine jasmine-core karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter

Update angular.json

Open your angular.json file and locate the test section under the architect block that references Karma or Jasmine. Remove or comment out these entries, as they are no longer needed. Here’s an example:

angular.json
{
  // ...
  "test": {
    "builder": "@angular-devkit/build-angular:karma",
    "options": {
      // ...
    }
  }
}

Clean Up Test Scripts

In your package.json, look for "scripts" entries that invoke Karma (e.g., "ng test" or anything referencing "karma.conf.js"). Remove them or replace them with Jest commands, which we’ll set up next.

package.json
{
  "scripts": {
    "test": "jest"
  }
}

Jest & Angular

Install

Set up Jest for Angular 19 by installing the following packages:

npm install --save-dev jest @types/jest jest-preset-angular ts-node

Project Configuration

In your monorepo, each Angular project (app or library) may have unique testing requirements. Instead of using a single top-level Jest configuration, it’s recommended to have a dedicated jest.config.ts file for each project. This approach allows you to tailor the testing setup to the specific needs of each package while maintaining better modularity and flexibility.

Config

Create a jest.config.ts file in each project folder, such as /projects/app, within your monorepo to enable project-specific configurations:

jest.config.ts
import type { Config } from "jest";

const config: Config = {
  preset: "jest-preset-angular",
  setupFilesAfterEnv: ["<rootDir>/setup-jest.ts"],
};

export default config;

Setup

The setup-jest.ts file, like the jest.config.ts file, should be placed in each project folder within your monorepo to ensure project-specific test environment configurations. Include the following code in the setup-jest.ts file as it is required:

setup-jest.ts
import { setupZoneTestEnv } from "jest-preset-angular/setup-env/zone";

setupZoneTestEnv();

Types

Replace "types": ["jasmine"] with "types": ["jest"] in your tsconfig.spec.json to switch from Jasmine to Jest type definitions:

tsconfig.spec.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "../../out-tsc/spec",
    "types": ["jest"]
  },
  "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
}

Verifying the Jest Setup

Before diving into Neovim configuration, make sure Jest works:

1. Sample Test

Check if a test file exists (e.g., app.component.spec.ts), and create one if it doesn’t.

2. Run Tests

cd projects/demo
npx jest

3. Validation

Verify that Jest executes successfully and your tests pass (or fail, if intended). It should look something like this:

npx jest
 PASS  src/app/app.component.spec.ts
  AppComponent
    ✓ should create the app (112 ms)
    ✓ should have the demo title (12 ms)
    ✓ should render title (19 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        1.477 s, estimated 2 s

Setting Up Neovim for Neotest

Enable Neotest (LazyVim Extras)

Neotest is a Neovim plugin that streamlines test discovery, execution, and result visualization - all from within your editor. To enable the Neotest framework in LazyVim, add the test.core`` extras to your LazyVim configuration. In many setups, this is done by editing~/.config/nvim/lua/config/lazy.lua` (or whichever Lua file you’ve designated for LazyVim plugin declarations):

lazy.lua
return {
  -- This line imports LazyVim’s built-in testing extras, which includes nvim-neotest/neotest:
  { import = "lazyvim.plugins.extras.test.core" },

  -- Other LazyVim or custom plugins go here
}

When Neovim loads, LazyVim will automatically pull in nvim-neotest/neotest and any related tooling, such as the default test adapters. You can then configure additional details—like the specific neotest-jest adapter—in separate plugin configuration files.

Validation

Press <leader>t in Neovim to confirm Neotest is enabled; a test command overlay should appear:

Neotest Overlay

Configure Neotest-Jest

To connect your Jest-based tests with Neotest, you need to create or edit the plugin configuration for the neotest-jest adapter. For example, in ~/.config/nvim/lua/plugins/test.lua:

~/.config/nvim/lua/plugins/test.lua
return {
  "nvim-neotest/neotest",
  dependencies = {
    "nvim-neotest/neotest-jest",
  },
  opts = {
    adapters = {
      ["neotest-jest"] = {
        jestConfigFile = function(path)
          return require("utils.path").get_project_root(path) .. "jest.config.ts"
        end,
        env = { CI = true },
        cwd = function(path)
          return require("utils.path").get_project_root(path)
        end,
      },
    },
  },
  -- stylua: ignore
  keys = {
    { "<leader>t",  "", desc = "+test" },
    { "<leader>tt", function() require("neotest").run.run(vim.fn.expand("%")) end,              desc = "Run File" },
    { "<leader>tT", function() require("neotest").run.run(require("utils.path").current_project_root()) end,       desc = "Run Project" },
    { "<leader>tA", function() require("neotest").run.run(vim.uv.cwd()) end,                    desc = "Run All" },
    { "<leader>tr", function() require("neotest").run.run() end,                                desc = "Run Nearest" },
    { "<leader>tl", function() require("neotest").run.run_last() end,                           desc = "Run Last" },
    { "<leader>ts", function() require("neotest").summary.toggle() end,                         desc = "Toggle Summary" },
    { "<leader>to", function() require("neotest").output.open({ enter = true, auto_close = true }) end, desc = "Show Output" },
    { "<leader>tO", function() require("neotest").output_panel.toggle() end,                    desc = "Toggle Output Panel" },
    { "<leader>tS", function() require("neotest").run.stop() end,                               desc = "Stop" },
    { "<leader>tw", function() require("neotest").watch.toggle(vim.fn.expand("%")) end,         desc = "Toggle Watch" },
  },
}

This setup includes:

utils.path

The utils.path module is a custom Lua utility that dynamically determines the project root and ensures correct paths for monorepo structures. Save it in ~/.config/nvim/lua/utils/path.lua:

~/.config/nvim/lua/utils/path.lua
local M = {}

function M.find_root(path)
  local root = path:match("(.-/[^/]+/)src")
  if root then
    return root
  end

  root = path:match("(.-/(projects)/[^/]+)") or path:match("(.-/(apps)/[^/]+)") or path:match("(.-/(libs)/[^/]+)")
  if root then
    return root
  end

  return vim.fn.getcwd()
end

function M.get_absolute_path(path)
  return vim.fn.fnamemodify(path, ":p")
end

function M.get_project_root(file_path)
  return M.get_absolute_path(M.find_root(file_path))
end

function M.current_project_root()
  return M.get_project_root(vim.fn.expand("%"))
end

return M

By leveraging utils.path, this setup ensures accurate handling of jest.config.ts and the working directory in a monorepo.

Run All Tests

If you want to run tests across all files in the entire monorepo, you can set up shared configurations at the root level. This is optional but useful for running all tests with a single command. Here’s an example:

jest.config.ts
import type { Config } from "jest";

const config: Config = {
  preset: "jest-preset-angular",
  roots: ["<rootDir>/projects"], // Points to all projects
  setupFilesAfterEnv: ["<rootDir>/setup-jest.ts"], // Global setup
};

export default config;
setup-jest.ts
import { setupZoneTestEnv } from "jest-preset-angular/setup-env/zone";

setupZoneTestEnv();
tsconfig.spec.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/spec",
    "types": ["jest"]
  },
  "include": ["projects/**/*.spec.ts", "projects/**/*.d.ts"]
}

With this setup, you can run all tests with <leader>tA.

Known Issue: Backticks in it() Descriptions

Using backticks (`) in it() or describe() may prevent Neotest from displaying error messages correctly. To avoid this issue, use single (') or double (") quotes for test descriptions.

Migrating Tests to Jest

If you’re transitioning from other testing frameworks like Jasmine to Jest, refer to the Jest Migration Guide for detailed steps and examples.

Conclusion

By adopting Jest in your Angular 19 monorepo and using Neotest in Neovim, you gain speedier tests, straightforward configuration, and an efficient coding workflow. This approach eliminates the quirks of older testing frameworks, saving you time and effort. Additionally, Neotest’s inline feedback keeps you fully in the zone while coding.