I’m the kind of developer who likes to stay in the loop of the technologies they love, even though they may not be working with them right now. And I love Angular.
Recently, the Angular team released its version 20, which comes with many improvements, largely discussed and explained in other sources, but there’s one improvement I haven’t found enough information about: its experimental support for Vitest.
There has been Vitest support for Angular for a while, thanks to Analog and its plugin, which I also tried when Angular 18 came out, and it works just fine. But Angular’s own support for Vitest is the new thing, so I had to learn how to use it. Luckily, Angular already has enough information in its docs to achieve this.
So, I had to test this new version in a personal project to learn how to use all these new things along with other technologies and tools I’ve used for a long time and also love, like VSCode, ESLint, Prettier, and a bunch of other. And I did, but there’s a catch which makes the whole “experimental” thing have more sense: you can only use this through ng test
.
This is fine, but I’m also the kind of developer who likes to use every tool to improve my DX. So, I needed to make this work at least with Vitest’s own command npx vitest
and with the Vitest official VSCode extension.
To achieve this, I had to deal with many errors and follow a bunch of steps to reverse-engineer Angular’s code, but I’ll save you the time and just show you the results.
I’ll assume you already have a working Angular 20 project with its Vitest support configured and running, and the starter App component with its corresponding spec file.
src/test-setup.ts
import { NgModule } from '@angular/core';
import {
ɵgetCleanupHook as getCleanupHook,
getTestBed
} from '@angular/core/testing';
import {
BrowserTestingModule,
platformBrowserTesting
} from '@angular/platform-browser/testing';
import { afterEach, beforeEach } from 'vitest';
const providers: NgModule['providers'] = [];
beforeEach(getCleanupHook(false));
afterEach(getCleanupHook(true));
@NgModule({ providers })
export class TestModule {}
getTestBed().initTestEnvironment(
[BrowserTestingModule, TestModule],
platformBrowserTesting(),
{
errorOnUnknownElements: true,
errorOnUnknownProperties: true
}
);
vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
root: './',
globals: true,
setupFiles: ['src/test-setup.ts'],
environment: 'jsdom',
watch: false,
reporters: ['default'],
coverage: {
enabled: false,
excludeAfterRemap: true
}
},
plugins: [
{
name: 'angular-coverage-exclude',
configureVitest(context) {
context.project.config.coverage.exclude = ['**/*.{test,spec}.?(c|m)ts'];
}
}
]
});
src/app/app.spec.ts
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { App } from './app';
describe('App', () => {
beforeAll(async () => {
try {
if (typeof process !== 'undefined' && process.versions?.node) {
const { readFileSync } = await import('node:fs');
const { ɵresolveComponentResources: resolveComponentResources } =
await import('@angular/core');
await resolveComponentResources(url =>
Promise.resolve(readFileSync(new URL(url, import.meta.url), 'utf-8'))
);
}
} catch {
return;
}
});
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [provideZonelessChangeDetection()]
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, ng20');
});
});
While this approach works outside of ng test
as desired (as of version 20.0.0), it’s a method that comes with its complications:
- It’s highly dependable in an experimental feature, so, along with the expected changes it will experience, this approach may easily stop working.
- It uses Angular internal variables, which are also subject to change unexpectedly.
- As you might have noticed, it has been tested in a very limited scenario, so there’s a really high possibility there will be edge cases where this fails.
I provide this code only for the fun of making it work with the tools I mentioned, and maybe as a way to open a discussion on finding better ways to do the same with less risks.
Any constructive criticism is welcome. Thanks for reading!
Top comments (0)