Dependency Injection Avanzata in Angular
La Dependency Injection (DI) è il cuore architetturale di Angular. Fin dalla versione 2,
il framework ha offerto un sistema di DI potente e sofisticato, ma con Angular 14+ e l'avvento dei
componenti standalone, il sistema si è evoluto radicalmente. Nuove API come inject(),
provider tree-shakable, guard e interceptor funzionali hanno trasformato il modo in cui gestiamo
le dipendenze nelle applicazioni Angular moderne.
In questo nono articolo della serie Angular Moderno, esploreremo la Dependency Injection
avanzata: dalla nuova funzione inject() ai token personalizzati, dalla gerarchia degli
injector ai pattern multi-provider, fino al testing e alla creazione dinamica di componenti con
contesto di injection. Se padroneggi già le basi della DI, questo articolo ti portera al livello
successivo.
Cosa Imparerai
- La funzione
inject()e perchè sostituisce l'iniezione via costruttore - Come creare e utilizzare
InjectionTokentipizzati - La gerarchia completa degli injector: Root, Platform, Element, Component
- Le opzioni
providedIne il loro impatto sul bundle - Il pattern
MULTIper interceptor, guard e provider multipli - Provider tree-shakable e ottimizzazione del bundle size
- Guard e interceptor funzionali con
CanActivateFnewithInterceptors() EnvironmentInjectore creazione dinamica di componentirunInInjectionContextper funzioni utility coninject()- Testing della DI con
TestBedeoverrideProvider
Panoramica della Serie Angular Moderno
| # | Articolo | Focus |
|---|---|---|
| 1 | Angular Signals | Reattività fine-grained |
| 2 | Zoneless Change Detection | Eliminare Zone.js |
| 3 | Nuovi Template @if, @for, @defer | Control flow moderno |
| 4 | Standalone Components | Architettura senza NgModule |
| 5 | Signal Forms | Form reattivi con Signals |
| 6 | SSR e Incremental Hydration | Server-Side Rendering |
| 7 | Core Web Vitals in Angular | Performance e metriche |
| 8 | Angular PWA | Progressive Web App |
| 9 | Sei qui - Dependency Injection Avanzata | DI tree-shakable |
| 10 | Migrare da Angular 17 a 21 | Guida alla migrazione |
1. La Funzione inject(): Addio Costruttore
La funzione inject(), introdotta come API stabile in Angular 14, rappresenta il
cambiamento più significativo nella DI moderna. Invece di dichiarare le dipendenze come parametri
del costruttore, si utilizza una funzione che può essere chiamata ovunque esista un
contesto di injection attivo: nel costruttore, negli inizializzatori di campo,
nelle factory function e nelle funzioni utility.
// PRIMA: Iniezione via costruttore (approccio tradizionale)
@Component({
selector: 'app-user-list',
standalone: true,
template: '...'
})
export class UserListComponent {
constructor(
private userService: UserService,
private router: Router,
private route: ActivatedRoute,
@Inject(API_URL) private apiUrl: string,
@Optional() private logger?: LoggerService
) {}
}
// DOPO: Funzione inject() (approccio moderno)
@Component({
selector: 'app-user-list',
standalone: true,
template: '...'
})
export class UserListComponent {
private userService = inject(UserService);
private router = inject(Router);
private route = inject(ActivatedRoute);
private apiUrl = inject(API_URL);
private logger = inject(LoggerService, { optional: true });
// I campi sono già disponibili, non serve il costruttore
users = this.userService.getUsers();
}
Vantaggi di inject() rispetto al Costruttore
| Aspetto | Costruttore | inject() |
|---|---|---|
| Leggibilita | Parametri lunghi, difficili da gestire | Dichiarazione inline, chiara e concisa |
| Ereditarieta | Richiede super() con tutti i parametri |
Nessun problema, i campi sono autonomi |
| Riutilizzo | Solo nei costruttori di classi Angular | Utilizzabile in funzioni, factory, utility |
| Type safety | Completa con decoratori | Completa, inferenza automatica del tipo |
| Tree-shaking | Limitato dai metadata del decoratore | Ottimale, reference diretta alla classe |
| Optional/Self/SkipSelf | Decoratori: @Optional(), @Self() |
Opzioni oggetto: { optional: true, self: true } |
Dove si può usare inject()
La funzione inject() funziona SOLO in un contesto di injection.
Chiamarla al di fuori di questo contesto genera un errore runtime
NG0203: inject() must be called from an injection context.
- Consentito: costruttori di componenti/servizi/direttive/pipe, inizializzatori di campo, factory function di provider, funzioni eseguite in
runInInjectionContext() - Non consentito: metodi di componenti chiamati dopo l'inizializzazione, callback di eventi, setTimeout/setInterval, funzioni standalone senza contesto
Un pattern particolarmente potente è l'uso di inject() per creare
funzioni utility riutilizzabili che incapsulano logica di injection:
// utils/navigation.ts
export function injectRouteParams() {
const route = inject(ActivatedRoute);
return toSignal(
route.paramMap.pipe(
map(params => ({
id: params.get('id'),
slug: params.get('slug')
}))
)
);
}
export function injectQueryParams() {
const route = inject(ActivatedRoute);
return toSignal(route.queryParamMap);
}
// Uso nel componente
@Component({ ... })
export class ProductDetailComponent {
params = injectRouteParams();
// params() restituisce { id: '123', slug: 'my-product' }
}
2. InjectionToken: Token Tipizzati per Valori Non-Classe
Quando la dipendenza non è una classe ma un valore primitivo (stringa, numero, oggetto di
configurazione), Angular non può usare la classe stessa come chiave di lookup.
InjectionToken risolve questo problema creando token univoci e tipizzati che
fungono da chiave nel registro della DI.
import { InjectionToken, inject } from '@angular/core';
// Token con valore statico
export const API_BASE_URL = new InjectionToken<string>(
'API_BASE_URL'
);
// Token con factory function (tree-shakable!)
export const IS_MOBILE = new InjectionToken<boolean>(
'IS_MOBILE',
{
providedIn: 'root',
factory: () => {
const platform = inject(PLATFORM_ID);
if (isPlatformBrowser(platform)) {
return window.innerWidth < 768;
}
return false;
}
}
);
// Token per configurazione complessa
export interface AppConfig {
apiUrl: string;
production: boolean;
features: {
darkMode: boolean;
analytics: boolean;
};
}
export const APP_CONFIG = new InjectionToken<AppConfig>(
'APP_CONFIG',
{
providedIn: 'root',
factory: () => ({
apiUrl: 'https://api.example.com',
production: false,
features: {
darkMode: true,
analytics: true
}
})
}
);
// Uso nei componenti
@Component({ ... })
export class AppComponent {
private config = inject(APP_CONFIG);
private isMobile = inject(IS_MOBILE);
}
Quando Usare InjectionToken
- Valori di configurazione: URL API, feature flags, timeout, limiti
- API del browser:
window,document,localStorage(per compatibilità SSR) - Implementazioni intercambiabili: strategia di logging, adapter di storage, provider di autenticazione
- Factory dinamiche: valori calcolati a runtime che dipendono da altri servizi
3. La Gerarchia degli Injector
Uno degli aspetti più potenti (e complessi) della DI in Angular e la sua struttura gerarchica. Non esiste un singolo contenitore di dipendenze: Angular mantiene un albero di injector che rispecchia la struttura dell'applicazione. Quando un componente richiede una dipendenza, Angular risale l'albero fino a trovarla.
Gerarchia degli Injector in Angular
+---------------------------------------------+
| Platform Injector |
| (condiviso tra applicazioni Angular) |
| Provider: PLATFORM_ID, PLATFORM_INITIALIZER|
+----------------------+----------------------+
|
+----------------------v----------------------+
| Root Injector |
| (providedIn: 'root', app.config.ts) |
| Provider: HttpClient, Router, servizi root |
+----------------------+----------------------+
|
+---------------+---------------+
| |
+------v------+ +-------v-----+
| Module | | Module |
| Injector | | Injector |
| (lazy mod.) | | (lazy mod.) |
+------+------+ +------+------+
| |
+------v------+ +-------v-----+
| Element | | Element |
| Injector | | Injector |
| (component) | | (component) |
+------+------+ +------+------+
| |
+------v------+ +-------v-----+
| Element | | Element |
| Injector | | Injector |
| (child) | | (child) |
+-------------+ +-------------+
Livelli della Gerarchia
| Livello | Scope | Creazione | Uso Tipico |
|---|---|---|---|
| Platform | Tutte le app Angular nella pagina | platformBrowserDynamic() |
Servizi condivisi tra micro-frontend |
| Root | Intera applicazione (singleton) | providedIn: 'root' o app.config.ts |
HttpClient, Router, servizi globali |
| Module | Modulo lazy-loaded | Array providers del modulo |
Servizi specifici di una feature |
| Element | Componente e suoi figli | Array providers del componente |
Stato locale, istanze multiple |
La risoluzione delle dipendenze segue un percorso preciso: Angular parte dall'injector
del componente che richiede la dipendenza e risale l'albero fino a trovare un provider
corrispondente. Se nessun provider viene trovato, viene generato un errore
NullInjectorError (a meno che la dipendenza non sia marcata come opzionale).
// Un servizio che gestisce lo stato locale
@Injectable()
export class FormStateService {
private fields = signal<Map<string, any>>(new Map());
private dirty = signal(false);
setValue(key: string, value: any) {
this.fields.update(m => {
const newMap = new Map(m);
newMap.set(key, value);
return newMap;
});
this.dirty.set(true);
}
isDirty = this.dirty.asReadonly();
}
// Ogni istanza del componente riceve il proprio FormStateService
@Component({
selector: 'app-editable-card',
standalone: true,
providers: [FormStateService], // Istanza LOCALE
template: `
<div class="card" [class.dirty]="formState.isDirty()">
<ng-content />
</div>
`
})
export class EditableCardComponent {
protected formState = inject(FormStateService);
}
4. Le Opzioni providedIn
Il decoratore @Injectable() accetta l'opzione providedIn che
determina in quale injector il servizio viene registrato automaticamente. Questa opzione
è fondamentale per il tree-shaking: Angular può eliminare i servizi non
utilizzati dal bundle finale.
Opzioni providedIn Disponibili
| Valore | Scope | Tree-shakable | Caso d'uso |
|---|---|---|---|
'root' |
Singleton per l'intera applicazione | Si | Servizi globali (Auth, API, State) |
'platform' |
Condiviso tra applicazioni Angular | Si | Micro-frontend, Elements |
'any' |
Un'istanza per modulo lazy-loaded | Si | Stato isolato per feature lazy |
| Componente | Istanza per componente | No | Stato locale, form, dialog |
// Singleton globale - l'approccio più comune
@Injectable({ providedIn: 'root' })
export class AuthService {
private currentUser = signal<User | null>(null);
isAuthenticated = computed(() => this.currentUser() !== null);
}
// Condiviso tra app Angular sulla stessa pagina
@Injectable({ providedIn: 'platform' })
export class SharedThemeService {
theme = signal<'light' | 'dark'>('dark');
}
// Un'istanza per ogni modulo lazy-loaded
@Injectable({ providedIn: 'any' })
export class FeatureAnalyticsService {
private events: AnalyticsEvent[] = [];
// Ogni feature lazy ha il proprio tracker indipendente
}
// Istanza per componente (non tree-shakable)
@Injectable() // Nessun providedIn
export class DialogStateService {
isOpen = signal(false);
data = signal<any>(null);
}
// Va registrato nell'array providers del componente:
@Component({
providers: [DialogStateService]
})
export class MyDialogComponent {
state = inject(DialogStateService);
}
Attenzione a providedIn: 'any'
L'opzione 'any' crea un'istanza separata per ogni modulo lazy-loaded.
Questo può causare comportamenti inaspettati se ti aspetti un singleton. Usala solo
quando vuoi esplicitamente isolare lo stato tra feature modules. Con l'architettura
standalone e le route lazy, 'any' è raramente necessario:
preferisci 'root' per i singleton o i providers del componente
per lo stato locale.
5. Multi Providers: Il Pattern MULTI
Il pattern multi-provider permette di associare più valori allo stesso token di injection. Quando si richiede il token, Angular restituisce un array con tutti i valori registrati. Questo pattern è alla base di funzionalità critiche come gli interceptor HTTP, i guard di routing e gli inizializzatori dell'applicazione.
import { InjectionToken, inject } from '@angular/core';
// Token multi-provider per validatori custom
export const FORM_VALIDATORS = new InjectionToken<FormValidator[]>(
'FORM_VALIDATORS'
);
export interface FormValidator {
name: string;
validate(value: any): string | null;
}
// Registrazione di più provider con multi: true
export const appConfig: ApplicationConfig = {
providers: [
{
provide: FORM_VALIDATORS,
useValue: {
name: 'required',
validate: (v: any) => v ? null : 'Campo obbligatorio'
},
multi: true
},
{
provide: FORM_VALIDATORS,
useValue: {
name: 'minLength',
validate: (v: string) =>
v?.length >= 3 ? null : 'Minimo 3 caratteri'
},
multi: true
},
{
provide: FORM_VALIDATORS,
useFactory: () => {
const http = inject(HttpClient);
return {
name: 'uniqueEmail',
validate: async (v: string) => {
// Validazione asincrona via API
return null;
}
};
},
multi: true
}
]
};
// Consumo nel servizio
@Injectable({ providedIn: 'root' })
export class ValidationService {
private validators = inject(FORM_VALIDATORS);
validateAll(value: any): string[] {
return this.validators
.map(v => v.validate(value))
.filter((err): err is string => err !== null);
}
}
Multi-Provider Integrati in Angular
HTTP_INTERCEPTORS- Interceptor HTTP (approccio class-based legacy)APP_INITIALIZER- Funzioni eseguite prima del bootstrap dell'appENVIRONMENT_INITIALIZER- Inizializzatori per environment injectorNG_VALIDATORS/NG_ASYNC_VALIDATORS- Validatori per formROUTES- Configurazione dinamica delle rotte
6. Provider Tree-Shakable
Uno dei vantaggi principali della DI moderna in Angular è il supporto per il
tree-shaking dei provider. Quando un servizio viene registrato con
providedIn: 'root' e la relativa factory nel decoratore, il bundler può
eliminarlo completamente dal build finale se nessun componente lo importa.
// TREE-SHAKABLE: il servizio si auto-registra
// Se nessun componente/servizio lo importa, viene eliminato dal bundle
@Injectable({ providedIn: 'root' })
export class AnalyticsService {
track(event: string) {
// Se AnalyticsService non e importato da nessuna parte,
// tutto questo codice viene rimosso dal bundle
console.log('Track:', event);
}
}
// TREE-SHAKABLE con factory: dipendenze calcolate
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>(
'FEATURE_FLAGS',
{
providedIn: 'root',
factory: () => {
const http = inject(HttpClient);
// La factory viene eliminata se FEATURE_FLAGS
// non viene mai iniettato
return { darkMode: true, newDashboard: false };
}
}
);
// NON TREE-SHAKABLE: registrato nell'array providers
// Sempre incluso nel bundle, anche se non usato
export const appConfig: ApplicationConfig = {
providers: [
LegacyService, // Sempre nel bundle!
{ provide: LoggerService, useClass: ConsoleLoggerService }
]
};
Impatto del Tree-Shaking sul Bundle
| Approccio | Registrazione | Tree-Shakable | Nel Bundle Se Non Usato |
|---|---|---|---|
providedIn: 'root' |
Nel decoratore del servizio | Si | No (eliminato) |
InjectionToken con factory |
Nel costruttore del token | Si | No (eliminato) |
Array providers |
In app.config.ts o componente | No | Si (sempre presente) |
NgModule providers |
Nel modulo | No | Si (sempre presente) |
Best Practice per il Tree-Shaking
- Usa sempre
providedIn: 'root'per i servizi globali - Definisci le factory nei token stessi, non nell'array providers
- Evita di registrare servizi nell'array
providersdiapp.config.tsa meno che non sia necessario (override, multi-provider) - Monitora il bundle size con
ng build --stats-jsonewebpack-bundle-analyzer
7. Guard e Interceptor Funzionali
A partire da Angular 15, guard e interceptor si sono evoluti da classi con interfacce
a semplici funzioni. Questo approccio funzionale riduce il boilerplate,
migliora la composabilità e sfrutta nativamente la funzione inject().
7.1 Guard Funzionali
// PRIMA: Guard come classe (deprecato)
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(
private auth: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean | UrlTree {
if (this.auth.isAuthenticated()) {
return true;
}
return this.router.createUrlTree(['/login']);
}
}
// DOPO: Guard come funzione
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
// Guard con parametri: factory function
export function roleGuard(requiredRole: string): CanActivateFn {
return (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.hasRole(requiredRole)) {
return true;
}
return router.createUrlTree(['/forbidden']);
};
}
// Configurazione delle rotte
export const routes: Routes = [
{
path: 'dashboard',
canActivate: [authGuard],
loadComponent: () => import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent)
},
{
path: 'admin',
canActivate: [authGuard, roleGuard('admin')],
loadComponent: () => import('./admin/admin.component')
.then(m => m.AdminComponent)
}
];
7.2 Interceptor Funzionali
import { HttpInterceptorFn } from '@angular/common/http';
// Interceptor per autenticazione
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.getToken();
if (token) {
const cloned = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next(cloned);
}
return next(req);
};
// Interceptor per logging
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const startTime = Date.now();
return next(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
const duration = Date.now() - startTime;
console.log(`${req.method} ${req.url} - ${duration}ms`);
}
})
);
};
// Interceptor per retry con backoff
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
retry({
count: 3,
delay: (error, retryCount) => {
if (error.status === 429 || error.status >= 500) {
return timer(Math.pow(2, retryCount) * 1000);
}
throw error;
}
})
);
};
// Registrazione in app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
authInterceptor,
loggingInterceptor,
retryInterceptor
])
)
]
};
Interceptor Class-based vs Funzionali
| Aspetto | Class-based (legacy) | Funzionali (moderno) |
|---|---|---|
| Sintassi | Classe + interfaccia HttpInterceptor |
Semplice funzione HttpInterceptorFn |
| Registrazione | HTTP_INTERCEPTORS multi-provider |
withInterceptors([]) |
| DI | Costruttore | inject() |
| Composizione | Difficile da parametrizzare | Factory function per parametri |
| Tree-shaking | Limitato | Ottimale |
8. EnvironmentInjector e Componenti Dinamici
L'EnvironmentInjector è un tipo di injector introdotto in Angular 14 che
opera a livello di "ambiente" (root, route, modulo lazy) piuttosto che a livello di
elemento DOM. E' fondamentale quando si creano componenti dinamicamente con
createComponent() o ViewContainerRef, poichè permette ai
componenti creati di accedere al contesto di injection corretto.
import {
Component,
EnvironmentInjector,
createEnvironmentInjector,
createComponent,
ViewContainerRef,
inject
} from '@angular/core';
// Servizio per creare componenti con un contesto DI personalizzato
@Injectable({ providedIn: 'root' })
export class DynamicComponentService {
private parentInjector = inject(EnvironmentInjector);
createIsolatedComponent<T>(
component: Type<T>,
vcr: ViewContainerRef,
providers: StaticProvider[] = []
) {
// Crea un nuovo EnvironmentInjector con provider aggiuntivi
const childInjector = createEnvironmentInjector(
[
{ provide: DIALOG_DATA, useValue: { title: 'My Dialog' } },
...providers
],
this.parentInjector
);
// Crea il componente nel nuovo contesto
const componentRef = createComponent(component, {
environmentInjector: childInjector,
hostElement: document.createElement('div')
});
// Inserisci nella view
vcr.insert(componentRef.hostView);
// Importante: distruggi l'injector quando il componente viene distrutto
componentRef.onDestroy(() => childInjector.destroy());
return componentRef;
}
}
// Uso in un componente host
@Component({
selector: 'app-dialog-host',
standalone: true,
template: `<ng-container #outlet />`
})
export class DialogHostComponent {
private dynamicService = inject(DynamicComponentService);
private vcr = inject(ViewContainerRef);
openDialog(dialogComponent: Type<any>, data: any) {
this.dynamicService.createIsolatedComponent(
dialogComponent,
this.vcr,
[{ provide: DIALOG_DATA, useValue: data }]
);
}
}
Quando Usare EnvironmentInjector
- Dialog/Modal dinamici: componenti creati a runtime che necessitano di dati iniettati
- Plugin system: caricamento dinamico di componenti con provider isolati
- Micro-frontend: ogni micro-app ha il proprio contesto di injection
- Testing avanzato: creare contesti di injection isolati per i test
9. runInInjectionContext: Injection Ovunque
La funzione runInInjectionContext(), introdotta in Angular 16, permette di
eseguire codice arbitrario all'interno di un contesto di injection. Questo risolve uno
dei limiti di inject(): la possibilità di chiamarla solo durante
la fase di costruzione. Con runInInjectionContext(), qualsiasi funzione
può accedere alle dipendenze.
import {
runInInjectionContext,
EnvironmentInjector,
inject,
DestroyRef
} from '@angular/core';
// Funzione utility che richiede inject()
function createAutoSaveEffect(formId: string) {
const http = inject(HttpClient);
const destroyRef = inject(DestroyRef);
const intervalId = setInterval(() => {
const formData = getFormData(formId);
http.post('/api/autosave', formData).subscribe();
}, 30000);
// Cleanup automatico quando il contesto viene distrutto
destroyRef.onDestroy(() => clearInterval(intervalId));
}
// Uso nel componente: funziona nel costruttore (contesto attivo)
@Component({ ... })
export class EditorComponent {
constructor() {
createAutoSaveEffect('editor-form');
}
}
// Uso al di fuori del costruttore: runInInjectionContext
@Component({ ... })
export class DashboardComponent {
private envInjector = inject(EnvironmentInjector);
onTabSelected(tabId: string) {
// Questo metodo viene chiamato da un click handler,
// quindi inject() non funzionerebbe direttamente
runInInjectionContext(this.envInjector, () => {
createAutoSaveEffect(tabId);
});
}
}
// Pattern avanzato: creare un "service locator" basato su funzioni
export function createServiceAccessor<T>(token: ProviderToken<T>) {
let cachedInstance: T | undefined;
return {
init: () => {
cachedInstance = inject(token);
},
get: () => {
if (!cachedInstance) {
throw new Error('Service not initialized. Call init() first.');
}
return cachedInstance;
}
};
}
Attenzione all'Uso di runInInjectionContext
Sebbene potente, runInInjectionContext() non dovrebbe essere abusato.
Usalo quando hai una ragione concreta per eseguire codice con injection al di fuori
della fase di costruzione. Se ti ritrovi a usarlo frequentemente, probabilmente
il design del componente può essere migliorato. Ricorda inoltre che l'injector passato
deve essere un EnvironmentInjector (non un ElementInjector),
quindi i provider registrati a livello di componente non saranno accessibili.
10. Testing della Dependency Injection
Uno dei grandi vantaggi della DI è la testabilità. Angular fornisce strumenti potenti per
configurare, sostituire e verificare le dipendenze nei test unitari. Con l'approccio
moderno basato su inject() e componenti standalone, il setup dei test
è diventato significativamente più semplice.
import { TestBed } from '@angular/core/testing';
describe('UserService', () => {
let service: UserService;
let httpMock: jest.Mocked<HttpClient>;
beforeEach(() => {
// Mock dell'HttpClient
const mockHttp = {
get: jest.fn(),
post: jest.fn()
};
TestBed.configureTestingModule({
providers: [
UserService,
{ provide: HttpClient, useValue: mockHttp },
{ provide: API_BASE_URL, useValue: 'http://test-api.com' }
]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpClient) as jest.Mocked<HttpClient>;
});
it('should fetch users from the API', () => {
const mockUsers = [{ id: 1, name: 'Alice' }];
httpMock.get.mockReturnValue(of(mockUsers));
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
expect(httpMock.get).toHaveBeenCalledWith(
'http://test-api.com/users'
);
});
});
});
// Test di un componente standalone con override
describe('DashboardComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DashboardComponent],
})
.overrideComponent(DashboardComponent, {
set: {
providers: [
{
provide: AnalyticsService,
useValue: { track: jest.fn() }
}
]
}
})
.compileComponents();
});
it('should track page view on init', () => {
const fixture = TestBed.createComponent(DashboardComponent);
const analytics = fixture.debugElement.injector.get(AnalyticsService);
fixture.detectChanges();
expect(analytics.track).toHaveBeenCalledWith('dashboard_view');
});
});
// Test di guard funzionali
describe('authGuard', () => {
it('should redirect to login when not authenticated', () => {
TestBed.configureTestingModule({
providers: [
{
provide: AuthService,
useValue: { isAuthenticated: () => false }
},
provideRouter([])
]
});
const injector = TestBed.inject(EnvironmentInjector);
// Esegui il guard nel contesto di injection
const result = runInInjectionContext(injector, () =>
authGuard(
{} as ActivatedRouteSnapshot,
{ url: '/dashboard' } as RouterStateSnapshot
)
);
expect(result).toBeInstanceOf(UrlTree);
});
it('should allow access when authenticated', () => {
TestBed.configureTestingModule({
providers: [
{
provide: AuthService,
useValue: { isAuthenticated: () => true }
},
provideRouter([])
]
});
const injector = TestBed.inject(EnvironmentInjector);
const result = runInInjectionContext(injector, () =>
authGuard(
{} as ActivatedRouteSnapshot,
{ url: '/dashboard' } as RouterStateSnapshot
)
);
expect(result).toBe(true);
});
});
Pattern di Testing per la DI
| Scenario | Metodo | Esempio |
|---|---|---|
| Sostituire un servizio | provide + useValue |
{ provide: AuthService, useValue: mockAuth } |
| Override a livello componente | overrideComponent |
Provider locali del componente |
| Testare InjectionToken | provide + useValue |
{ provide: API_URL, useValue: 'http://test' } |
| Testare guard funzionali | runInInjectionContext |
Eseguire il guard nel contesto TestBed |
| Spy su metodi reali | inject + spyOn |
spyOn(TestBed.inject(Svc), 'method') |
Best Practice per il Testing della DI
- Preferisci mock manuali a
jasmine.createSpyObjper un controllo migliore sui tipi - Testa i servizi in isolamento: mock tutte le dipendenze, testa solo la logica del servizio
- Usa
TestBed.inject()per accedere ai servizi registrati nel modulo di test - Per guard e interceptor funzionali:
runInInjectionContext()con l'injector del TestBed - Verifica gli errori: testa che
NullInjectorErrorvenga generato quando mancano provider obbligatori
Riepilogo e Prossimi Passi
In questo articolo abbiamo esplorato la Dependency Injection avanzata in Angular,
dalla nuova API inject() fino al testing. La DI moderna in Angular e
molto più di un semplice meccanismo di creazione di oggetti: è un sistema sofisticato
che influenza l'architettura, le performance e la manutenibilità dell'applicazione.
Concetti Chiave
- La funzione
inject()sostituisce l'iniezione via costruttore, offrendo migliore leggibilità, riutilizzo e tree-shaking InjectionTokenpermette di iniettare valori non-classe con tipizzazione forte e factory tree-shakable- La gerarchia degli injector (Platform, Root, Module, Element) determina scope e lifetime delle dipendenze
providedIn: 'root'e l'opzione preferita per servizi singleton tree-shakable- Il pattern
multi: trueè alla base di interceptor, guard e inizializzatori - Guard funzionali (
CanActivateFn) e interceptor funzionali (HttpInterceptorFn) riducono il boilerplate e migliorano la composabilità EnvironmentInjectorecreateEnvironmentInjector()permettono la creazione dinamica di componenti con contesti DI isolatirunInInjectionContext()permette di usareinject()al di fuori della fase di costruzione- Il testing della DI sfrutta
TestBed,overrideComponenterunInInjectionContextper mock e verifica completi
Nel prossimo e ultimo articolo della serie, Migrare da Angular 17 a 21, vedremo come applicare tutte le conoscenze acquisite in questa serie per migrare un'applicazione esistente verso le API moderne di Angular, con una guida passo-passo che copre ogni aspetto della transizione.
Domande frequenti
Cos'è la Dependency Injection (DI) e perché è importante in Angular?
La DI è il cuore architetturale di Angular. È un sistema potente e sofisticato che gestisce le dipendenze delle applicazioni Angular, consentendo una separazione tra la logica dell'applicazione e quella della sua implementazione.
Quali sono i vantaggi principali del nuovo sistema DI introdotto in Angular 14+?
Il nuovo sistema DI offre un approccio più flessibile e personalizzabile, con funzionalità come la creazione di token tipizzati per valori non-classe, la gerarchia degli injector e il tree-shaking dei provider.
Come si utilizza la funzione inject() nella Dependency Injection avanzata?
La funzione inject() può essere chiamata ovunque esista un contesto di injection attivo, come nel costruttore, negli inizializzatori di campo o nelle factory function. Funziona solo all'interno di un contesto di injection e genera un errore runtime se utilizzata al di fuori.
Qual è il ruolo della gerarchia degli injector nella Dependency Injection avanzata?
La gerarchia degli injector rappresenta la struttura dell'applicazione Angular, con ogni componente che richiede una dipendenza che risale l'albero fino a trovare un provider corrispondente. Se nessun provider viene trovato, viene generato un errore (a meno che la dipendenza non sia marcata come opzionale).

Commenti
Caricamento commenti...
Accedi per commentare