Salta al contenuto principale

9 - Dependency Injection Avanzata in Angular

Padroneggia la DI avanzata in Angular: inject() function, InjectionToken, multi providers, functional guards/interceptors, EnvironmentInjector e testing.

Cosa ne pensi di questo articolo?

9 - Dependency Injection Avanzata in Angular
Angular ModernoFoto:Bibek ghoshPexels

In Breve

Padroneggia la DI avanzata in Angular: inject() function, InjectionToken, multi providers, functional guards/interceptors, EnvironmentInjector e testing.

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 InjectionToken tipizzati
  • La gerarchia completa degli injector: Root, Platform, Element, Component
  • Le opzioni providedIn e il loro impatto sul bundle
  • Il pattern MULTI per interceptor, guard e provider multipli
  • Provider tree-shakable e ottimizzazione del bundle size
  • Guard e interceptor funzionali con CanActivateFn e withInterceptors()
  • EnvironmentInjector e creazione dinamica di componenti
  • runInInjectionContext per funzioni utility con inject()
  • Testing della DI con TestBed e overrideProvider

Panoramica della Serie Angular Moderno

# Articolo Focus
1Angular SignalsReattività fine-grained
2Zoneless Change DetectionEliminare Zone.js
3Nuovi Template @if, @for, @deferControl flow moderno
4Standalone ComponentsArchitettura senza NgModule
5Signal FormsForm reattivi con Signals
6SSR e Incremental HydrationServer-Side Rendering
7Core Web Vitals in AngularPerformance e metriche
8Angular PWAProgressive Web App
9Sei qui - Dependency Injection AvanzataDI tree-shakable
10Migrare da Angular 17 a 21Guida 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.

Iniezione via costruttore vs inject()

// 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:

Funzioni utility con inject()

// 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.

Creare e utilizzare InjectionToken

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).

Injection a livello di componente: istanze multiple

// 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
Confronto tra le opzioni providedIn

// 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.

Creare e utilizzare multi-provider

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'app
  • ENVIRONMENT_INITIALIZER - Inizializzatori per environment injector
  • NG_VALIDATORS / NG_ASYNC_VALIDATORS - Validatori per form
  • ROUTES - 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 vs Non tree-shakable

// 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 providers di app.config.ts a meno che non sia necessario (override, multi-provider)
  • Monitora il bundle size con ng build --stats-json e webpack-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

Guard: da classi a funzioni

// 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

Interceptor funzionali con withInterceptors()

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.

Creare un EnvironmentInjector personalizzato

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.

Uso di runInInjectionContext()

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.

Testing con TestBed e override dei provider

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.createSpyObj per 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 NullInjectorError venga 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
  • InjectionToken permette 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à
  • EnvironmentInjector e createEnvironmentInjector() permettono la creazione dinamica di componenti con contesti DI isolati
  • runInInjectionContext() permette di usare inject() al di fuori della fase di costruzione
  • Il testing della DI sfrutta TestBed, overrideComponent e runInInjectionContext per 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).

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti