5 Things to Know About Migrating Angular Tests to Vitest (After Moving 20+ Repositories)
Cristian Sifuentes\
4 min read · Feb 24, 2026
I recently completed migrating the unit tests of more than 40
repositories that power a large Angular certification ecosystem to
Vitest.
This wasn't a toy migration.\
It involved real-world test suites, legacy Jasmine patterns, async RxJS
flows, DOM-heavy component tests, and CI pipelines.
What follows is not a tutorial.
It's what actually changes --- and what doesn't --- when you move from
Jasmine/Karma (or Jest) to Vitest in Angular 21+.
Vitest Is 99% the Same Syntax --- And That's the Point
Consider this test:
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display four tabs', () => {
const tabs = fixture.debugElement.queryAll(By.css('button'));
expect(tabs.length).toBe(4);
});
What framework is this?
- Jasmine\
- Jest\
- Vitest
Answer: All of them.
Modern JS testing ecosystems converged toward:
-
describe -
it -
expect - matchers like
toBe,toEqual,toBeTruthy
The migration is therefore not conceptual.
It's mechanical.
Angular CLI Migration Handles the Boring Part
Here's the mapping that matters:
Jasmine Vitest
fit it.only
fdescribe describe.only
xit it.skip
xdescribe describe.skip
spyOn vi.spyOn
jasmine.createSpy vi.fn()
fail() vi.fail()
jasmine.objectContaining expect.objectContaining
jasmine.any expect.any
Automate it:
ng g @schematics/angular:refactor-jasmine-vitest
This removes most manual friction instantly.
Async Tests Are Where Real Migration Happens
Jasmine-style async test:
it("should return all movies by default", (done) => {
service.filterMovieList().subscribe(movies => {
expect(movies.length).toBe(2);
done();
});
});
Vitest version:
it("should return all movies by default", async () => {
vi.useFakeTimers();
let moviesReceived = [];
service.filterMovieList()
.subscribe(movies => moviesReceived = movies);
await vi.runAllTimersAsync();
expect(moviesReceived.length).toBe(2);
});
Key differences:
- Explicit timer control\
- No
done()callbacks\ - Deterministic execution
Under heavy CI parallelization, this matters.
Browser Mode Is Explicit
Vitest can run in a browser like Karma --- but not by default.
If your tests rely on:
-
window -
localStorage - real layout calculations
You must enable browser mode in config.
Flexibility is power --- but it requires awareness.
Component Testing Becomes Ergonomic
Traditional Angular unit test setup:
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BasketComponent]
}).compileComponents();
fixture = TestBed.createComponent(BasketComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
Vitest-style component testing:
test('should render clear basket button', async ({ page, mount }) => {
await mount(Basket);
await expect(
page.getByRole('button', { name: 'Clear' })
).toBeVisible();
});
This reduces boilerplate and increases readability.
It feels closer to real user interaction.
What Actually Required Manual Fixes
- Async RxJS tests\
- Custom Jasmine matchers\
- Implicit zone timing assumptions\
- Legacy global spy patterns\
- CI adjustments
What didn't break:
- Pure function tests\
- Basic component logic tests\
- Snapshot-style DOM assertions
Strategic Takeaways
Vitest improves:
- Startup speed\
- Parallel execution\
- Async determinism\
- Mocking ergonomics\
- Component testing experience
But test quality still depends on:
- Lifecycle awareness\
- Isolation discipline\
- Architectural clarity
No runner fixes bad test design.
Final Thoughts
The ecosystem is converging.
Syntax differences are fading.
What matters now:
- Determinism\
- Speed\
- Developer ergonomics\
- Scalability
Vitest delivers on those fronts in Angular 21+.
---\
Cristian Sifuentes\
Angular Architect · Performance & Testing Specialist

Top comments (0)