Angular v21 and the End of TS-Only Test Coverage: Why We Must Rethink Component Testing
In this post, we’ll explore a significant change in Angular v21 that affects how we test our components. Angular v21 introduces Vitest as the default testing library, replacing the long-standing Jasmine/Karma setup. While this might seem like just a tooling update, it has deep implications for our testing strategies.
The real change is about aligning with modern Angular style guidelines, proper encapsulation, and accurate test coverage.
If you recently upgraded to Angular v21 (or switched to Vitest) and suddenly noticed coverage gaps that didn’t exist before, don’t worry. We’ll explore why this is happening and how to adapt your tests to meet Angular’s expectations.
This is an opinionated article about the next steps with testing with Angular v21+.
The Legacy Mental Model: TypeScript-Only Coverage
For a long time, Angular applications were tested using:
- Jasmine + Karma
- Jest-based setups
Regardless of the runner, coverage was effectively TypeScript-only:
.tsfiles were instrumented.htmltemplates were usually ignored

This led to a common testing pattern where developers would:
- Call component methods directly (since they were public)
- Read or mutate component properties in tests (since they were public)
- Assert on internal state
As long as the method executed, coverage increased, even if the template never exercised that logic.
The result? High coverage metrics that looked great on paper but did not accurately reflect the reliability of the UI.
Angular v21 + Vitest: Templates Are Now Part of Coverage
Angular v21 switches the default testing setup to Vitest. The most significant change lies in how coverage is reported:
HTML templates are now included in coverage reports.

What does this mean in practice? Coverage now tracks:
- Event bindings like
(click)and(change) - Conditional rendering (
@ifand other controls) - Structural directives
- Template-driven execution paths
As a result:
- Calling a component method directly no longer “covers” template logic
- Coverage gaps now appear exactly where the UI is not being exercised
This is not a regression. It’s Angular providing a more accurate and honest view of your application’s health.
The Impact of Protected Members
Since Angular v20, the Angular style guide recommends:
Prefer protected access for any members that are meant to be read from the component’s template.
This adds another challenge to the legacy testing approach. If a member is protected, it’s not accessible in your test file. You can’t simply reach into the component and check a property or call a method directly.
What does this mean? Even if you wanted to bypass the template and test the logic directly, TypeScript won’t let you.
So we have two forces pushing us in the same direction:
- Vitest requires template execution for coverage
- Angular Style Guide restricts access to internal component logic
The message is clear: Stop testing implementation details. Start testing behavior.
A Real Example: ProductListComponent
To illustrate this, let’s look at a real example. The ProductListComponent from my modern-angular repository demonstrates this pattern:
app/products/product-list
This component was designed following modern Angular standards:
- Template-driven logic: The template handles user interactions
- Protected members: Properties and methods used only in the template are marked as
protected - Minimal public API: The component exposes only what’s strictly necessary
This design follows the recommendations the Angular style guide has been encouraging since v20.
The Legacy Test Pattern (Pre-v21)
Before Angular v21, a typical test often looked like this:
it('filters products by category', () => {
component.selectedCategory = 'Books';
component.applyFilter();
expect(component.filteredProducts.length).toBe(2);
});
Why did this seem correct?
- The method executed successfully
- The assertions passed
- TypeScript coverage metrics increased
However, here’s what actually happened:
- The template was never involved in the test
- No user interaction (click, input, etc.) was simulated
- The test validated implementation details, not actual behavior
With Vitest, the HTML coverage remains untouched, and the coverage reports reveal the truth: the component was not fully tested.
The Challenge: Protected APIs Are Not Test-Friendly
In modern Angular components:
- Template-only properties and methods are marked
protected. - TypeScript correctly prevents direct access to these members from tests.
This creates friction for legacy test styles. Common workarounds include:
- Casting the component to
any. - Making methods
publicsolely for testing purposes.
But with template-aware coverage, these workarounds fail for two reasons:
- They violate encapsulation principles.
- They do not improve template coverage.
Angular is effectively telling us: If your test doesn’t go through the template, it doesn’t count.
The Solution: Test Through the Template
The solution is not to fight the framework, but to change how we test components.
Instead of invoking logic directly, we should:
- Interact with the DOM
- Trigger events
- Assert on the rendered output
Let’s refactor the previous test to follow this approach:
it('filters products when a category is selected', async () => {
const select = fixture.nativeElement.querySelector('select');
select.value = 'Books';
select.dispatchEvent(new Event('change'));
fixture.detectChanges();
const items = fixture.nativeElement.querySelectorAll('.product-item');
expect(items.length).toBe(2);
});
What’s happening here?
- The test interaction starts from the template (how users actually use your app)
- The protected logic remains protected (proper encapsulation)
- Coverage now includes both TypeScript and HTML
This test reflects how users actually interact with the component, ensuring a more robust and reliable application.
💡 Bonus: You get test coverage for both the HTML template and the TypeScript code within the component!
What Changes in Practice
With Angular v21+, your tests should:
- Prefer DOM interaction over method calls
- Assert visible behavior, not internal state
- Treat the template as the entry point
What you’ll gain from this shift:
- Tests become more refactor-friendly
- Encapsulation improves naturally
- Coverage numbers finally reflect real usage
This aligns perfectly with Angular’s design direction and modern testing best practices.
Conclusion
When upgrading to Angular v21 + Vitest, expect to:
- See coverage drops in UI-heavy components
- Identify tests that only exercise TS logic
- Rewrite tests to trigger behavior via the template once you make properties and methods protected
Pre-Angular v21 coverage with Jest:

Angular v21 with Vitest:

At first, this change feels uncomfortable. But in reality, Angular v21 is doing something important:
- Aligning style guidelines with tooling
- Encouraging behavior-driven testing
- Eliminating false confidence from TS-only coverage
Angular isn’t making testing harder. It’s making testing more honest.
Happy coding!
