Angular v21 and the End of TS-Only Test Coverage: Why We Must Rethink Component Testing

16 Dec 2025
5 mins read

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:

Regardless of the runner, coverage was effectively TypeScript-only:

placeholder

This led to a common testing pattern where developers would:

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.

placeholder

What does this mean in practice? Coverage now tracks:

As a result:

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:

  1. Vitest requires template execution for coverage
  2. 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:

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?

However, here’s what actually happened:

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:

This creates friction for legacy test styles. Common workarounds include:

But with template-aware coverage, these workarounds fail for two reasons:

  1. They violate encapsulation principles.
  2. 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:

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?

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:

What you’ll gain from this shift:

This aligns perfectly with Angular’s design direction and modern testing best practices.

Conclusion

When upgrading to Angular v21 + Vitest, expect to:

Pre-Angular v21 coverage with Jest:

placeholder

Angular v21 with Vitest:

placeholder

At first, this change feels uncomfortable. But in reality, Angular v21 is doing something important:

Angular isn’t making testing harder. It’s making testing more honest.

Happy coding!