😧 The Trouble with JIT
Whether you rely on Karma, Jest, or Vitest, your Angular tests have likely been running with Just-In-Time (JIT) compilation, since it was the sole option available until recently.
JIT brings along several notable drawbacks:
- Inaccurate code coverage, because templates are excluded from the analysis.
- Reduced speed, as templates are compiled during test execution.
- Limited longevity, given that Angular has pushed JIT to its boundaries. By design, certain features cannot function with JIT.
- Asymmetry with production, where AOT is the standard.
⏰ Why the Shift Now?
Starting with Angular 8 and the arrival of IVy, the Angular compiler began generating instructions from templates. A key benefit of this change was that coverage tools could now map those instructions back to the template and calculate coverage accordingly.
Although running tests with AOT to produce code coverage has been theoretically feasible since Angular 8, neither Karma nor Jest offered this capability. AOT for testing only became a reality with the addition of Vitest support by the Analog team.
As of November 2024:
- Vitest stands as the only option with AOT compilation support.
- Pull requests are in progress for Karma and the Jest Experimental Builder.
🎁 Further Advantages of AOT Testing
⚡️ Faster Test Execution
Regardless of using JIT or AOT, components must be compiled at some stage. The distinction lies in the fact that AOT performs the compilation once and caches the result, whereas JIT may recompile components for each test module.
Consequently, even if the transformation phase takes slightly longer with AOT, the total test execution time decreases. Reported figures suggest roughly 20% faster execution, though this heavily depends on your test structure and the System Under Test.
👯 Production-Symmetry
Ideally, tests should mirror the production environment as closely as possible for greater confidence. This goal often conflicts with other priorities, such as test speed, the size of the System Under Test, or predictability.
AOT uniquely enhances production-symmetry without sacrificing those other qualities. Adopting AOT leads to higher confidence and behavior that more closely matches production.
🔮 Future-Proof Tests
More critically, JIT has hit its ceiling and is becoming a burden for Angular. Certain features, like Deferrable Views, simply don't work with JIT. Other potential Angular roadmap items, such as selectorless components, will likely remain incompatible with JIT.
In fact, since Angular's Signal Inputs (and other functional APIs) were introduced, JIT has already necessitated some minimal transformations.
Switching to AOT ensures your tests are ready for the future, able to take advantage of new innovations, and prepared for whatever direction JIT takes next.
🤔 Potential Downsides
🪄 Steering Clear of Dynamic Component Constructs
Enabling AOT will cause certain techniques built on dynamic constructs to fail.
For example, patterns like the following will no longer function:
// 🛑 This is broken with AOT.
const fixture = render(`<app-button/>`, { imports: [Button] });
function render(template, { imports }) {
@Component({
template,
imports,
})
class TestContainer {}
return TestBed.createComponent(TestContainer);
}
There is still a way to circumvent AOT compilation (⚠️ for now ⚠️):
function render(template, { imports }) {
@Component({
jit: true,
template,
imports,
})
class TestContainer {}
return TestBed.createComponent(TestContainer);
}
My recommendation is to minimize the use of such patterns and instead create test-specific components when the need arises, even if it adds some verbosity. The Angular team may eventually offer alternatives that are both AOT-friendly and less repetitive.
🦦 The Challenge with Shallow Testing
Even though Shallow Testing isn't ideal as your main strategy because it's less production-symmetric, it's still a valuable tool to have.
With AOT, overriding a component's imports via TestBed#overrideComponent is currently not possible.
As a workaround, you can override the component's dependencies at the module level using your testing framework's API and substitute components with test doubles.
Here's an example with Vitest:
// app.cmp.spec.ts
vi.mock('./street-map.cmp', async () => {
return {
StreetMap: await import('./street-map-fake.cmp').then(
(m) => m.StreetMapFake
),
};
});
// street-map-fake.cmp.ts
@Component({
selector: 'app-street-map',
template: 'Fake Street Map',
})
class StreetMapFake implements StreetMap {
// ...
}
While this interim solution is AOT-compatible, it has its trade-offs:
- It's less readable and more verbose.
- "Mocking" (or providing test doubles) at the module level is less precise and potentially less predictable.
- It's tightly integrated with your chosen testing framework.
For now, I'd suggest sticking with JIT for Shallow Tests until TestBed#overrideComponent gains AOT support or the Angular team offers a superior option. This can be done by setting up a distinct configuration for Shallow Tests that uses JIT for specs matching a pattern like *.jit.spec.ts.
👨🏻🍳 Trying Out Vitest with AOT
1. Setting up Vitest
- For Angular CLI projects, leverage Analog's schematic.
- For Nx projects, pick the
vitestoption when generating an app or library (offered since Nx 20.1.0).
2. Enabling AOT
Find the vite.config.js file and activate AOT by configuring Angular's plugin jit option to false:
export default defineConfig({
...
plugins: [
angular({ jit: false }),
...
],
...
});
📈 Setting Up Code Coverage
You have the choice between istanbul and native v8 for coverage. For reasons that are still being investigated, Vitest's coverage remapping doesn't work correctly with v8. The fix is to use istanbul instead.
1. Install @vitest/coverage-istanbul
Ensure the Vitest Istanbul package aligns with the major version of Vitest you're using.
npm install -D @vitest/coverage-istanbul
2. Set istanbul as your coverage provider
Modify your vite.config.mts to activate coverage through Istanbul:
export default defineConfig({
...
test: {
...
coverage: {
provider: 'istanbul',
},
},
});
You're now ready to execute the test suite:
nx test my-app --coverage --ui --watch
# or
ng test --coverage --ui --watch
Afterward, select the coverage icon and watch the template's coverage appear. 🤯
(The coverage report is also saved in the coverage directory.)
Bear in mind that coverage is determined from the compiler's generated instructions, which implies:
Structural directives are included as well.
And here's a bonus:
Inline templates are also covered! 🚀
Code coverage is a helpful signal, but treat it as a guideline rather than a strict target.
Once you apply pressure to a statistical regularity for control, it tends to break down.
-- Charles Goodhart
Simply put, when a metric becomes the objective, it loses its effectiveness as a metric.
It's worth adding that the most basic metrics often give the most distorted picture.
Karma users will soon enable AOT with a simple configuration flag.
For Jest users, there are three paths:
- Recommended: Switch to Vitest. (📻 keep an ear out—I'll soon share the smoothest transition strategy)
- Leverage the experimental builder with AOT.
- Await
jest-preset-angularAOT support.
Vitest users can already take advantage of AOT today. 🎉
- 💻 Demo Repository
- 📝 Angular AOT Compiler Documentation
- 📝 Vitest Documentation
- 📝 Analog's Vitest Guide
If you're dealing with persistent bugs or flaky tests that break during every refactor, the Pragmatic Angular Testing video course is your solution.
Gain practical, dependable testing techniques to ensure your Angular projects stay steady and easy to maintain. (Currently available at 50% off for a limited period!)





