name: ng-register-vitest description: Step-by-step guide for migrating Angular projects from Karma/Jasmine to Vitest (Angular v20/21+). Includes dependency management, configuration setup, test file API migration, and best practices for zoneless/OnPush testing.
Register Vitest and Migrate from Karma/Jasmine
Overview
This skill guides coding agents through migrating an existing Angular project from Karma + Jasmine to Vitest with modern patterns (OnPush + Zoneless). The process involves removing legacy dependencies, configuring vitest, and updating test APIs.
Target Environment:
- Angular v20.2+
- TypeScript 5.7+
- Node.js 20+
Benefits:
- ✅ 10-50x faster test execution (no Zone.js overhead)
- ✅ Native ESM support with esbuild
- ✅ Modern async/await patterns
- ✅ Zoneless-compatible from the start
- ✅ Smaller bundle size in tests
Prerequisites
Before starting, verify:
- Angular version is 20.2 or later
- Project is buildable (
ng buildcompletes) - All tests currently pass (baseline for comparison)
- Git is initialized (to track migration diffs)
Check Angular version:
ng version
# Output should show: Angular: 20.x or 21.x
Step 1: Remove Karma/Jasmine Dependencies
1a. Identify and remove dependencies
Run this to find Karma and Jasmine packages:
npm list | grep -E "(karma|jasmine)"
1b. Uninstall Karma and Jasmine
npm uninstall \
@angular-devkit/build-angular \
@angular/cdk \
@types/jasmine \
jasmine-core \
karma \
karma-chrome-launcher \
karma-coverage \
karma-jasmine \
karma-jasmine-html-reporter
Alternative (Windows PowerShell):
npm uninstall `
@angular-devkit/build-angular `
@angular/cdk `
@types/jasmine `
jasmine-core `
karma `
karma-chrome-launcher `
karma-coverage `
karma-jasmine `
karma-jasmine-html-reporter
1c. Verify removal
npm list | grep -E "(karma|jasmine)"
# Should return empty
Step 2: Install Vitest Dependencies
2a. Install required packages
npm install --save-dev \
vitest@^4.0.0 \
@analogjs/vitest-angular@^1.18.0 \
jsdom@^26.0.0
2b. Verify installation
npm list vitest @analogjs/vitest-angular jsdom
Expected output: Should show all three packages installed
Step 3: Create vitest.config.ts
Create file at project root:
File: vitest.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
include: ['src/**/*.spec.ts'],
setupFiles: ['src/test-setup.ts'],
environment: 'jsdom',
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'json'],
exclude: ['node_modules/', 'dist/']
}
},
});
Key options:
globals: true– No need to importdescribe,it,expectinclude– Pattern for test files (must match*.spec.ts)setupFiles– Files to run before tests (TestBed initialization)jsdom– Browser environment simulation for testing
Step 4: Create Test Setup File
Create file at src/test-setup.ts:
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
import { getTestBed } from '@angular/core/testing';
let testBedInitialized = false;
if (!testBedInitialized) {
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
testBedInitialized = true;
}
This initializes the TestBed environment before tests run.
Step 5: Update tsconfig.spec.json
Update TypeScript types to use Vitest instead of Jasmine:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"vitest/globals"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
Changes from Karma/Jasmine:
- Remove:
"types": ["jasmine"] - Add:
"types": ["vitest/globals"]
Step 6: Update angular.json Test Configuration
Update the test builder in angular.json:
{
"projects": {
"my-app": {
"architect": {
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json",
"buildTarget": "my-app:build"
}
}
}
}
}
}
Using Angular CLI to update (recommended):
Run this command to regenerate the test configuration automatically:
ng build --configuration=test
Or manually edit:
- Find
"test"builder (currently points to@angular-devkit/build-angular:karma) - Replace with
"@angular/build:unit-test" - Simplify options:
- Remove
karmaConfigproperty - Keep only
tsConfigandbuildTarget
- Remove
Step 7: Update package.json Test Script
In package.json, verify the test script:
{
"scripts": {
"test": "ng test"
}
}
This automatically uses @angular/build:unit-test (Vitest) via angular.json.
Step 8: Migrate Test File APIs
8a. Spy and Mock Syntax
| Jasmine | Vitest | Reason |
|---|---|---|
spyOn(obj, 'method') |
vi.spyOn(obj, 'method') |
Vitest spy syntax |
.and.returnValue(x) |
.mockReturnValue(x) |
Mock implementation |
.and.callFake(fn) |
.mockImplementation(fn) |
Custom mock logic |
jasmine.createSpyObj() |
{ method: vi.fn() } |
Manual mock objects |
Example Migration:
// Before (Jasmine)
beforeEach(() => {
const userServiceSpy = jasmine.createSpyObj('UserService', ['getUser']);
userServiceSpy.getUser.and.returnValue(of({ id: 1, name: 'Test' }));
});
// After (Vitest)
beforeEach(() => {
const userServiceSpy = {
getUser: vi.fn().mockReturnValue(of({ id: 1, name: 'Test' }))
};
});
8b. Async Patterns
| Jasmine | Vitest | Notes |
|---|---|---|
waitForAsync() |
async/await |
Native async/await |
fakeAsync() |
vi.useFakeTimers() |
Manual timer control |
tick(ms) |
vi.advanceTimersByTime(ms) |
Advance timers |
flush() |
vi.runAllTimers() |
Run all pending |
flushMicrotasks() |
await Promise.resolve() |
Flush microtasks |
Example Migration:
// Before (Jasmine with fakeAsync)
it('waits for async operation', fakeAsync(() => {
component.loadData();
tick(1000);
expect(component.data).toBeDefined();
}));
// After (Vitest with timers)
it('waits for async operation', () => {
vi.useFakeTimers();
component.loadData();
vi.advanceTimersByTime(1000);
expect(component.data).toBeDefined();
vi.useRealTimers();
});
// Or with native async/await
it('waits for async operation', async () => {
component.loadData();
await new Promise(resolve => setTimeout(resolve, 1000));
expect(component.data).toBeDefined();
});
8c. DOM and Assertion Changes
| Jasmine | Vitest |
|---|---|
toBeTrue() |
toBe(true) |
toBeFalse() |
toBe(false) |
toHaveSize(n) |
toHaveLength(n) |
.innerText |
.textContent (jsdom) |
toContain(a, b) |
expect([a, b]).toContain(x) |
Step 9: Remove Old Karma Configuration Files
Delete these files if they exist:
rm -f karma.conf.js # Karma config
rm -f src/karma-test-shim.js # Old test shim
Or in PowerShell:
Remove-Item -Path 'karma.conf.js' -ErrorAction SilentlyContinue
Remove-Item -Path 'src/karma-test-shim.js' -ErrorAction SilentlyContinue
Step 10: Run Tests and Validate
10a. Execute tests
npm test
# or
ng test
10b. Expected output
✓ src/app/app.component.spec.ts (3)
✓ src/app/services/user.service.spec.ts (5)
...
✓ 40 files, 127 tests passed (0.5s)
10c. Check test coverage
npm test -- --coverage
Step 11: Update All Test Files (Batch Approach)
For projects with many tests, use a systematic approach:
11a. Identify all test files
find src -name "*.spec.ts" -type f | wc -l
11b. Audit API usage in tests
# Find jasmine.createSpyObj usage
grep -r "jasmine.createSpyObj" src --include="*.spec.ts"
# Find fakeAsync usage
grep -r "fakeAsync" src --include="*.spec.ts"
# Find spyOn usage (needs vi.spyOn)
grep -r "spyOn" src --include="*.spec.ts"
11c. Migration priority
- First: Test utility files and base specs
- Second: Service specs (no UI dependencies)
- Third: Component specs (depends on refactored services)
- Last: Integration tests
Common Issues and Solutions
Issue 1: Test Hangs or Timeout
Symptom: Tests timeout or hang after migration
Solution:
- Check for missing
vi.useRealTimers()after fake timer tests - Ensure
await fixture.whenStable()is used for async operations - Verify no
setTimeoutwithout proper cleanup inafterEach
// ✅ Correct
afterEach(() => {
vi.useRealTimers(); // Reset timers
vi.clearAllMocks(); // Clear all spies
});
Issue 2: DOM not Updating in Tests
Symptom: Template changes not reflected in test assertions
Solution:
- Use
await fixture.whenStable()instead of manualtick() - Call
fixture.detectChanges()after state changes
// ✅ Correct for OnPush components
it('updates on input change', async () => {
fixture.componentRef.setInput('data', newData);
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain('updated');
});
Issue 3: AsyncScheduler RxJS Tests Failing
Symptom: RxJS asyncScheduler tests fail with real timers
Solution:
Use vi.useFakeTimers() for async scheduler operations:
it('emits on asyncScheduler', () => {
vi.useFakeTimers();
// AsyncScheduler operations here
vi.runAllTimers();
expect(...).toBe(...);
vi.useRealTimers();
});
Issue 4: TestBed is Not Initialized
Symptom: Error: "TestBed is not initialized"
Solution:
- Verify
src/test-setup.tsexists - Check
vitest.config.tsincludessetupFiles: ['src/test-setup.ts'] - Rebuild project:
npx tsc --noEmit
Issue 5: Vitest Can't Find Module
Symptom: Error: "Cannot find module '@angular/core'"
Solution:
- Clear node_modules:
rm -rf node_modules && npm install - Verify all dependencies are installed
- Check paths in
tsconfig.jsonare correct
Testing with Zoneless
For applications using provideZonelessChangeDetection():
// In your test setup or individual tests
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent],
providers: [provideZonelessChangeDetection()]
}).compileComponents();
});
it('works with zoneless', async () => {
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
await fixture.whenStable();
// Signal updates trigger change detection automatically
component.count.set(5);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('5');
});
Best Practices for Vitest Migration
✅ Use Modern Patterns
// Good: Direct signal testing
it('signal updates', () => {
const count = signal(0);
count.set(5);
expect(count()).toBe(5);
});
// Good: Using async/await
it('loads data', async () => {
await component.loadData();
expect(component.data).toBeDefined();
});
❌ Avoid Legacy Patterns
// Bad: Relying on zone.js
it('handles zone events', (done) => {
setTimeout(() => {
expect(...).toBe(...);
done(); // Don't use done callback
}, 100);
});
✅ Proper Cleanup
afterEach(() => {
vi.clearAllMocks();
vi.useRealTimers();
vi.clearAllTimers();
});
Verification Checklist
- Dependencies removed:
npm list | grep karmareturns empty - Dependencies installed:
npm list vitestshows v4+ -
vitest.config.tsexists at project root -
src/test-setup.tsexists and initializes TestBed -
tsconfig.spec.jsonhas"types": ["vitest/globals"] -
angular.jsontest builder is@angular/build:unit-test - No
karma.conf.jsin project root -
npm testruns without errors - Test output shows: "X files, X tests passed"
- All test APIs updated (no
jasmine., nofakeAsync)