| 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 build completes)
- All tests currently pass (baseline for comparison)
- Git is initialized (to track migration diffs)
Check Angular version:
ng version
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)"
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
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 import describe, it, expect
include – 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
karmaConfig property
- Keep only
tsConfig and buildTarget
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:
beforeEach(() => {
const userServiceSpy = jasmine.createSpyObj('UserService', ['getUser']);
userServiceSpy.getUser.and.returnValue(of({ id: 1, name: 'Test' }));
});
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:
it('waits for async operation', fakeAsync(() => {
component.loadData();
tick(1000);
expect(component.data).toBeDefined();
}));
it('waits for async operation', () => {
vi.useFakeTimers();
component.loadData();
vi.advanceTimersByTime(1000);
expect(component.data).toBeDefined();
vi.useRealTimers();
});
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
rm -f src/karma-test-shim.js
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
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
grep -r "jasmine.createSpyObj" src --include="*.spec.ts"
grep -r "fakeAsync" src --include="*.spec.ts"
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
setTimeout without proper cleanup in afterEach
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
});
Issue 2: DOM not Updating in Tests
Symptom: Template changes not reflected in test assertions
Solution:
- Use
await fixture.whenStable() instead of manual tick()
- Call
fixture.detectChanges() after state changes
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();
vi.runAllTimers();
expect(...).toBe(...);
vi.useRealTimers();
});
Issue 4: TestBed is Not Initialized
Symptom: Error: "TestBed is not initialized"
Solution:
- Verify
src/test-setup.ts exists
- Check
vitest.config.ts includes setupFiles: ['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.json are correct
Testing with Zoneless
For applications using provideZonelessChangeDetection():
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();
component.count.set(5);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('5');
});
Best Practices for Vitest Migration
✅ Use Modern Patterns
it('signal updates', () => {
const count = signal(0);
count.set(5);
expect(count()).toBe(5);
});
it('loads data', async () => {
await component.loadData();
expect(component.data).toBeDefined();
});
❌ Avoid Legacy Patterns
it('handles zone events', (done) => {
setTimeout(() => {
expect(...).toBe(...);
done();
}, 100);
});
✅ Proper Cleanup
afterEach(() => {
vi.clearAllMocks();
vi.useRealTimers();
vi.clearAllTimers();
});
Verification Checklist
References