Here, we take a look at how to test Angular applications that rely on the inject function to resolve dependencies.
Prefer watching over reading? A video demonstration is available below:
Why inject?
Since Angular 14, the inject function has served as a substitute for constructor-based dependency injection. It offers the following benefits:
1. Consistency of Decorators
In the TypeScript environment, decorators have historically operated under experimental conditions.
The standardization of decorators is complete at TC39, but every decorator kind still needs finishing before the TypeScript compiler's experimental flag can be turned off.
With dependency injection through the constructor, parameter decorators are required, which leaves room for issues down the line. In contrast, the inject function has no need for such decorators, reducing the likelihood of future disruptions.
class FlightSearchComponent {
constructor(
@SkipSelf() // not standardized
@Optional() // not standardized
private flightService1: FlightService
) {}
private flightService2 = inject(FlightService, {
skipSelf: true,
optional: true,
});
}
2. Erased Type in the constructor
At runtime, the type of inject is still accessible. When a constructor is used, the compilation wipes out all type details other than the variable name. Consequently, the TypeScript compiler must inject specific metadata to preserve type information in the output. This behavior is not enabled by default in the compiler.
// TypeScript
class FlightService {}
class FlightSearchComponent {
constructor(private flightService: FlightService) {}
flightService2 = inject(FlightService);
}
// Compiled JavaScript
class FlightService {
}
class FlightSearchComponent {
// type is gone
constructor(flightService) {
this.flightService = flightService;
// type is still there
this.flightService2 = inject(FlightService);
}
}
3. Class Inheritance
When working with class hierarchies, inject proves far more convenient. In a constructor-based setup, every subclass must supply the dependencies required by its superclass. Using inject, however, lets each parent class resolve its own dependencies independently.
// Inheritance and constructor-based dependency injection
class Animal {
constructor(private animalService: AnimalService) {}
}
class Mammal extends Animal {
constructor(
private mammalService: MammalService,
animalService: AnimalService
) {
super(animalService);
}
}
class Cat extends Mammal {
constructor(
private catService: CatService,
mammalService: MammalService,
animalService: AnimalService
) {
super(mammalService, animalService);
}
}
// Inheritance via inject
class Animal {
animalService = inject(AnimalService);
}
class Mammal extends Animal {
mammalService = inject(MammalService);
}
class Cat extends Mammal {
catService = inject(CatService);
}
4. Type-Safe Injection Tokens
Injection tokens guarantee type safety.
const VERSION = new InjectionToken<number>('current version');
class AppComponent {
//compiles, although VERSION is of type number
constructor(@Inject('VERSION') unsafeVersion: string) {}
safeVersion: string = inject(VERSION); // fails 👍
}
5. Functional Approaches
Certain functional patterns—NgRx Store being a prime example—lack a constructor, making inject their only viable option.
Given those numerous benefits, inject quickly gained prominence. For a while, it seemed the constructor-based pattern could fall out of favor entirely.
However, the landscape has evolved as of now. Alex Rickabaugh notes that property decorators have reached Stage 1 in the standardization process. He therefore advises choosing the approach that suits your situation and awaiting the outcome of the TC39 discussions.
Timestamp: 25:25
TestBed.inject
When application code adopts inject, testing often hits a snag, particularly around how objects get created. With a constructor-based approach, a test could spin up a class instance directly; inject removes that option, forcing tests to route through the TestBed instead.
For any Service or @Injectable under test, TestBed.inject is available from any point within the test file.
It works whether called right at the start, midway through, or after the test logic has run.
Here is the Service we aim to test:
@Injectable({ providedIn: "root" })
export class AddressAsyncValidator {
#httpClient = inject(HttpClient);
validate(ac: AbstractControl<string>): Observable<ValidationErrors | null> {
return this.#httpClient
.get<unknown[]>("https://nominatim.openstreetmap.org/search.php", {
params: new HttpParams()
.set("format", "jsonv2").set("q", ac.value),
})
.pipe(
map((addresses) =>
addresses.length > 0 ? null : { address: "invalid" }
)
);
}
}
Since AddressAsyncValidator relies on HttpClient through injection, that dependency needs to be mocked.
Our TestingModule doesn't require a component to be imported or instantiated.
The scenario is purely about testing logic—no UI or DOM rendering is involved.
describe("AddressAsyncValidator", () => {
it("should validate an invalid address", waitForAsync(async () => {
TestBed.configureTestingModule({
providers: [
{
provide: HttpClient,
useValue: { get: () => of([]).pipe(delay(0)) },
},
],
});
const validator = TestBed.inject(AddressAsyncValidator);
const isValid = await lastValueFrom(
validator.validate({ value: "Domgasse 5" } as AbstractControl)
);
expect(isValid).toEqual({ address: "invalid" });
}));
});
The test passes, but two caveats deserve attention.
The first caveat: when AddressAsyncValidator lacks {providedIn: 'root'} and only @Injectable is present, adding the service to the TestingModule providers becomes mandatory:
@Injectable()
export class AddressAsyncValidator {
// ...
}
describe("AddressAsyncValidator", () => {
it("should validate an invalid address", waitForAsync(async () => {
TestBed.configureTestingModule({
providers: [
AddressAsyncValidator,
{
provide: HttpClient,
useValue: { get: () => of([]).pipe(delay(0)) },
},
],
});
// rest of the test
}));
});
The second obstacle is that inject can't be invoked directly within the test. Attempting to do so triggers the standard error response.
NG0203: inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with
runInInjectionContext.
describe("AddressAsyncValidator", () => {
it("should validate an invalid address", waitForAsync(async () => {
TestBed.configureTestingModule({
providers: [
AddressAsyncValidator,
{
provide: HttpClient,
useValue: { get: () => of([]).pipe(delay(0)) },
},
],
});
const validator = inject(AddressAsyncValidator); // not good
}));
});
TestBed.runInInjectionContext
So why would a test ever need to call inject? The simple answer is: any time the code under test relies on it.
Within Angular itself, that can be something like an HttpInterceptorFn or a router guard such as CanActivateFn.
Across the Angular ecosystem right now, functional-style patterns are popping up all over the place.
A sensible starting point could be
nxtensions
/
nxtensions
Extensions and plugins for Nx
Nxtensions
Nxtensions provides a collection of plugins and utilities designed for use with Nx.
Nx is a build framework known for its intelligence and extensibility. Its foundational capabilities include:
- Generating and analyzing project and task graphs.
- Running and coordinating tasks.
- Caching computations to avoid redundant work.
- Generating code efficiently.
The framework's base features can be expanded through plugins, enabling support for various frameworks and technologies that aren't covered by the standard Nx team plugins.
Package inventory
- @nxtensions/astro: An Astro plugin tailored for Nx.
- @nxtensions/tsconfig-paths-snowpack-plugin: A Snowpack plugin that enables the use of path mappings defined in tsconfig files.
Get involved
Want to contribute? We encourage participation, so be sure to review our Contributors Guide.
feat(vite-plugin-angular): enable `.analog` support
#870
PR Checklist
Go through the list and confirm your PR satisfies the criteria:
- [x] The commit message complies with the guidelines laid out in the contribution guide
- [x] Tests were added for the change (for features or bug resolutions)
- [ ] Docs were added or refreshed (for features or bug resolutions)
PR Type
What category of change is being introduced in this PR?
- [ ] Bugfix
- [x] Feature
- [ ] Code style adjustment (formatting, scoped variables)
- [ ] Refactor (no behavioral or API changes)
- [ ] Build-related modifications
- [ ] CI-related modifications
- [ ] Documentation updates
- [ ] Other... Please specify:
Which package are you modifying?
- [x] vite-plugin-angular
- [ ] vite-plugin-nitro
- [ ] astro-angular
- [ ] create-analog
- [ ] router
- [ ] platform
- [ ] content
- [ ] nx-plugin
- [ ] trpc
What is the new behavior?
With this PR, the .analog file extension becomes supported by toggling the supportAnalogFormat flag located under vite.experimental.
This supersedes the previous .ng file extension. The .analog extension provides a more explicit (and deliberate) distinction. Feature parity with .ng is maintained, apart from these differences:
-
templateUrlnow handles references to external HTML files -
styleUrlandstyleUrlsnow handle references to external stylesheet file(s) -
<style>blocks (for inline styling) are mapped to thestylesproperty in the Component metadata, rather than being injected into the<template>- Having several
<style>blocks isn't assured. For multiple stylesheets, rely onstyleUrlsinstead
- Having several
Does this PR introduce a breaking change?
- [ ] Yes
- [x] No
Other information
[optional] What gif best expresses this PR's impact or your sentiment?
However, we'll stick to built-in capabilities and test a CanActivateFn:
export const apiCheckGuard: CanActivateFn = (route, state) => {
const httpClient = inject(HttpClient);
return httpClient.get("/holiday").pipe(map(() => true));
};
The apiCheckGuard function serves a single purpose: it checks whether a request targeting the "/holiday" URL comes back successfully. Since this function lacks a constructor, the only way for it to gain access to dependencies is by calling inject.
Writing a test for this function might involve something like:
it("should return true", waitForAsync(async () => {
TestBed.configureTestingModule({
providers: [
{ provide: HttpClient, useValue: { get: () => of(true).pipe(delay(1)) } },
],
});
expect(await lastValueFrom(apiCheckGuard())).toBe(true);
}));
The same approach fails here as well, once again triggering the NG0203 error.
Instead, we need to reach for TestBed.runInInjectionContext. Its name is quite descriptive: it takes any function and executes it within the injection context, so any inject calls inside that function can resolve successfully.
describe("Api Check Guard", () => {
it("should return true", waitForAsync(async () => {
TestBed.configureTestingModule({
providers: [
{
provide: HttpClient,
useValue: { get: () => of(true).pipe(delay(1)) },
},
],
});
await TestBed.runInInjectionContext(async () => {
const value$ = apiCheckGuard();
expect(await lastValueFrom(value$)).toBe(true);
});
}));
});
At first glance, you might think TestBed.runInInjectionContext offers the injection context in an asynchronous manner — but it doesn't.
The guard invokes inject in a synchronous fashion. Had it been called inside the pipe operator, the inject would execute within the async task and encounter the same failure.
Summary
Compared to constructor-based dependency injection, inject offers a range of benefits, so adopting it is worth consideration.
For function-based constructs like HttpInterceptorFn and router guards, inject serves as the exclusive means to access the dependency injection system.
To make inject work properly, those function calls need to be wrapped in TestBed.runInInjectionContext.
Additionally, there's TestBed.inject, which serves a distinct purpose — it is exclusive to tests and is meant for retrieving class instances, regardless of whether those classes rely on inject or constructor-based injection.
The repository is available at https://github.com/rainerhahnekamp/how-do-i-test
If there's a testing problem you'd like me to cover here, feel free to reach out!
For more updates, follow me on LinkedIn and X, and check out our website for workshops and consulting on testing services.


