Quando si sviluppano applicazioni Angular, è fondamentale garantire che i componenti funzionino correttamente e che l’interazione con l'utente avvenga senza intoppi. Un metodo molto utile per testare il comportamento dei componenti in modo isolato e indipendente dalla loro implementazione è l’utilizzo delle component harness. Questo approccio, introdotto nell'Angular CDK (Component Dev Kit), consente di simulare e testare l’interazione dell'utente con i componenti senza dipendere dal DOM o dalla struttura interna dei componenti stessi.

Nel contesto di una libreria come Angular Material, le component harness offrono un’interfaccia ben definita per interagire con i componenti, permettendo di eseguire test in modo affidabile e indipendente. Questi strumenti sono progettati per essere utilizzati nel contesto di test automatizzati, ma la loro applicazione può essere molto utile anche durante lo sviluppo, per comprendere meglio come ogni singolo componente si comporta in determinate condizioni.

Ad esempio, consideriamo il caso di un’applicazione di e-commerce che permette di selezionare una maglia tramite un componente di selezione delle taglie e poi procedere all’acquisto tramite un pulsante. Il processo di test di questo flusso potrebbe essere svolto con le component harness di Angular, come mostrato nel test seguente:

  1. Importazione delle librerie necessarie: Per prima cosa, dobbiamo importare i moduli e le classi necessari per il nostro test, inclusi quelli di Angular Material per i pulsanti e le selezioni.

typescript
import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestBed } from '@angular/core/testing'; import { MatButtonModule } from '@angular/material/button'; import { MatButtonHarness } from '@angular/material/button/testing'; import { MatSelectModule } from '@angular/material/select'; import { MatSelectHarness } from '@angular/material/select/testing';
  1. Configurazione del modulo di test: Successivamente, configuriamo il modulo di test di Angular, dichiarando i componenti necessari e sostituendo eventuali servizi con versioni fittizie (spy services) che ci permettano di monitorare le chiamate ai metodi senza effettivamente eseguire logiche di business reali.

typescript
TestBed.configureTestingModule({ declarations: [ShirtComponent], imports: [MatButtonModule, MatSelectModule], providers: [{ provide: OrderService, useClass: OrderSpyService }], });
  1. Caricamento dei componenti di test: Una volta configurato l'ambiente, creiamo un loader che ci permette di interagire con il componente e di simulare azioni come un utente.

typescript
const fixture = TestBed.createComponent(ShirtComponent); const loader = TestbedHarnessEnvironment.loader(fixture);
  1. Simulazione delle azioni dell’utente: A questo punto possiamo simulare l'interazione dell'utente, come la selezione della taglia di una maglia e l'azione di acquisto tramite un pulsante. Utilizzeremo le component harness per ottenere i componenti giusti, come il selettore delle taglie e il pulsante di acquisto.

typescript
it('orders a Large shirt', async () => { const shirtSizePicker = await loader.getHarness(MatSelectHarness); const purchaseButton = await loader.getHarness(MatButtonHarness.with({ text: '1-click purchase' })); await shirtSizePicker.clickOptions({ text: 'Large' }); await purchaseButton.click('center'); });
  1. Verifica dell’effetto dell’interazione: L'ultima parte del test è la verifica che l’interazione abbia avuto l’effetto desiderato, ad esempio, controllando che il servizio di ordine sia stato chiamato correttamente.

typescript
expect(orderSpy.purchase).toHaveBeenCalledTimes(1);

Questo test dimostra come le component harness di Angular Material ci permettano di interagire con i componenti a livello logico, senza preoccuparci dei dettagli di implementazione o della struttura del DOM. Utilizzando un approccio di testing come questo, possiamo concentrarci sul comportamento del componente e sulla sua interazione con l'utente, piuttosto che sui dettagli interni.

Le component harness non solo semplificano i test, ma li rendono anche più resilienti alle modifiche strutturali del codice. Questo significa che, anche se la struttura interna dei componenti cambia in una futura versione di Angular Material, i test continueranno a funzionare correttamente, a condizione che il comportamento esterno del componente rimanga invariato.

Un altro aspetto importante da considerare è che le component harness sono progettate per testare i componenti in modo agnostico rispetto alla loro implementazione. Ciò significa che possiamo scrivere test per componenti di Angular Material senza dipendere da particolari tecnologie o strutture di codice. Questo approccio è particolarmente utile in contesti aziendali, dove la modularità e la flessibilità sono cruciali.

Per i lettori che vogliono comprendere a fondo l’utilizzo delle component harness, è importante notare che l’uso di queste ultime non è limitato ai componenti Angular Material. È possibile creare le proprie component harness per testare componenti personalizzati, estendendo la funzionalità offerta dall'Angular CDK. Inoltre, poiché le component harness sono progettate per essere indipendenti dal DOM, possono essere utilizzate per scrivere test che non siano influenzati dalle modifiche nella struttura del layout o nelle specifiche del componente.

Come progettare un'applicazione Angular modulare e scalabile per la gestione dei corsi scolastici

Quando si sviluppa un'applicazione complessa, è fondamentale separare il recupero e l'archiviazione dei dati dall'uso effettivo dei componenti. Questo approccio non solo migliora l'organizzazione, ma consente anche una maggiore manutenibilità e scalabilità. Prima di procedere con l'uso dei componenti Angular, è essenziale stabilire un modello dei dati che rappresenti correttamente le entità coinvolte nel sistema. In questo caso, ci concentreremo su un modello di dati relativamente semplice, utilizzando interfacce TypeScript per rappresentare le entità principali e sfruttando i servizi Angular per la comunicazione con il backend.

Il modello di dati che useremo include tre entità principali: Scuola, Corso e Video. Ogni entità è progettata per rappresentare in modo chiaro le informazioni essenziali per il caso d'uso dell'applicazione.

Per il Corso, il modello include un identificativo, un titolo, una descrizione opzionale e una lista di video associati al corso. Ogni video è associato a un identificativo esterno (ID di YouTube), un titolo, una data di pubblicazione, un autore e una descrizione facoltativa. La Scuola, invece, è caratterizzata da un identificativo, un nome, le coordinate geografiche (latitudine e longitudine) e un array di corsi offerti. Questo approccio consente di mappare in modo semplice e intuitivo le relazioni tra scuole, corsi e video.

Una volta definito il modello di dati, possiamo passare alla progettazione dell'applicazione, suddividendola in moduli. La suddivisione in moduli consente di separare le preoccupazioni, migliorare la leggibilità del codice e facilitare l'espansione futura dell'applicazione. L'applicazione sarà divisa in tre moduli principali: Corso, Tema e Scuole. Ogni modulo avrà il proprio componente Angular, che sarà responsabile di visualizzare e gestire una parte dell'interfaccia utente.

Il modulo del Corso utilizzerà un layout a griglia CSS, già introdotto nel Capitolo 5, per visualizzare il corso scelto dall'utente. Inoltre, sarà presente un componente Video che sfrutterà il modulo YouTube Player di Angular per mostrare i video da piattaforme esterne come YouTube. Il modulo Tema permetterà agli utenti di personalizzare l'aspetto dell'applicazione, influenzando l'intera interfaccia utente, mentre il modulo Scuole consentirà di selezionare una scuola e visualizzare i corsi offerti da quella scuola, tramite la componente Google Maps di Angular.

La navigazione tra i vari componenti sarà gestita dal router Angular, con rotte ben definite che collegheranno i moduli dell'applicazione. Per esempio, il percorso predefinito sarà associato al modulo Corso, e l'utente verrà reindirizzato automaticamente al primo corso disponibile. L'integrazione di un sistema di login potrebbe essere prevista in una versione futura, consentendo all'utente di memorizzare la sua scelta del corso.

Per quanto riguarda la gestione delle dipendenze, Angular permette di includere i moduli necessari solo quando sono richiesti, evitando così dipendenze superflue e semplificando la gestione del codice. Per esempio, il modulo Corso includerà solo il modulo Video e il modulo Tema, mentre il modulo Scuola dipenderà dal modulo Google Maps. Questa metodologia aiuta a mantenere l'applicazione snella e modulare, semplificando l'adozione di nuove funzionalità in futuro.

Per il recupero dei dati, saranno utilizzati i servizi Angular. Il servizio CourseService si occuperà di recuperare i dati relativi ai corsi, mentre SchoolsService recupererà i dati relativi alle scuole. Questi servizi utilizzeranno chiamate asincrone per ottenere i dati dal server, garantendo un'esperienza utente fluida e senza blocchi. Ad esempio, il servizio CourseService avrà un metodo getCourse che recupererà un singolo corso, mentre SchoolsService avrà un metodo getSchools per recuperare una lista di scuole. I dati saranno rappresentati utilizzando oggetti mock, ma in un'applicazione reale questi dati verranno recuperati dinamicamente dal backend.

Infine, la navigazione tra le diverse sezioni dell'applicazione sarà gestita attraverso un menu laterale che permetterà all'utente di accedere al modulo Corso, al modulo Scuole e alle impostazioni del Tema. Questo approccio garantisce un'interazione intuitiva e ben organizzata con l'applicazione.

Un aspetto fondamentale nella progettazione di un'applicazione come questa è la separazione delle preoccupazioni. Ogni modulo e componente ha una singola responsabilità, e l'accesso ai dati è gestito in modo centralizzato tramite i servizi. Questa architettura rende l'applicazione facilmente estendibile, mantenibile e testabile, due caratteristiche essenziali per ogni applicazione scalabile. Inoltre, l'uso di Angular per gestire le dipendenze e la navigazione assicura che l'applicazione rimanga organizzata anche man mano che cresce in complessità.

Come la compilazione Ahead-of-Time (AOT) di Angular migliora le prestazioni e la produttività

La compilazione Ahead-of-Time (AOT) di Angular, introdotta con la versione Ivy, ha introdotto una serie di miglioramenti che influenzano profondamente il flusso di lavoro di sviluppo, la velocità di esecuzione dell’applicazione e la gestione dei pacchetti di produzione. AOT è una tecnica che compila i template dei componenti Angular al momento della costruzione dell'applicazione, anziché al momento dell'esecuzione. Questo approccio riduce significativamente il tempo di avvio dell'applicazione e ottimizza l’uso delle risorse durante l'esecuzione.

Un aspetto fondamentale di AOT è l'adozione di un set di istruzioni Ivy, che permette di rimuovere dal pacchetto di produzione tutte le istruzioni inutilizzate. Per esempio, se un'applicazione non è multilingue, tutte le istruzioni relative all'internazionalizzazione vengono rimosse dal bundle finale. Allo stesso modo, se l'applicazione non utilizza animazioni, le relative istruzioni vengono escluse. Questo processo di tree-shaking, o "shaking dell’albero", consente di ridurre le dimensioni finali del bundle e migliorare la velocità di caricamento.

La differenza tra il motore di rendering tradizionale (View Engine) e Ivy è evidente anche nelle prestazioni di runtime. Mentre il View Engine richiede l'interpretazione dei dati strutturali della vista per poterli eseguire, Ivy è in grado di eseguire direttamente le istruzioni precompilate. Questo rende Ivy significativamente più veloce in fase di esecuzione, poiché elimina la necessità di un processo di interpretazione runtime, riducendo così il carico sulle risorse di sistema.

L’adozione di una compilazione Ahead-of-Time non si limita però solo ai benefici delle prestazioni. L’utilizzo di una compilazione anticipata dei template è anche fondamentale per migliorare la produttività degli sviluppatori. Attivando il controllo rigoroso dei tipi dei template (strict template type checking), come descritto nel Capitolo 2 del nostro libro, è possibile intercettare gran parte degli errori di tipo nelle metadati di Angular, nei modelli di componenti e nei template stessi. Questi errori verranno segnalati durante la costruzione dell’applicazione o direttamente nel nostro editor di codice tramite il servizio di linguaggio di Angular.

Anche nei test unitari, la compilazione Ahead-of-Time di Ivy apporta significativi vantaggi. La compilazione è più rapida rispetto al motore View Engine e include un sistema di cache che permette di conservare i moduli, componenti, direttive, pipe e servizi compilati tra un caso di test e l'altro. Con il View Engine, la compilazione anticipata non è supportata nei test unitari, mentre con Ivy il supporto AOT consente una maggiore velocità di compilazione e di esecuzione dei test.

Il caricamento delle applicazioni Angular è più veloce con Ivy, in quanto non è necessario includere il compilatore Just-in-Time (JIT) nel bundle finale. La compilazione JIT viene eseguita durante il runtime, il che porta a un avvio più lento dell'applicazione. Invece, con AOT, l'applicazione viene "bootstrapata" più velocemente perché il compilatore viene eseguito al momento della costruzione e non al momento dell'esecuzione. Le prestazioni sono ulteriormente ottimizzate dal set di istruzioni Ivy, che consente l'esecuzione immediata delle istruzioni precompilate.

Tuttavia, l’adozione della compilazione Ahead-of-Time non è priva di limiti. Uno dei principali svantaggi è che i componenti, direttive e pipe devono essere compilati prima dell'esecuzione, il che limita la possibilità di creare dinamicamente questi dichiarativi in fase di runtime, come ad esempio in base a configurazioni server-side o file di configurazione statici. L’unica soluzione sarebbe quella di includere il compilatore Angular nel pacchetto finale dell’applicazione, ma questo vanificherebbe i vantaggi della compilazione anticipata.

Un altro limite riguarda la risoluzione delle dipendenze dinamiche. Mentre le dipendenze iniettate, come i servizi basati su classi, possono essere risolte al runtime, le dipendenze asincrone non possono essere gestite direttamente. Per risolvere tale problema, le dipendenze asincrone devono essere incapsulate in servizi basati su classi, funzioni, promise o observable, consentendo comunque di rispettare la compatibilità con la compilazione AOT.

Quando si utilizzano funzioni o metodi statici per determinare la dichiarazione dei metadati, come ad esempio l’importazione o la dichiarazione di moduli Angular, si possono verificare errori di compilazione, in quanto Angular AOT non supporta l’utilizzo di funzioni complesse per la dichiarazione dei metadati. In questi casi, è necessario rifattorizzare il codice in modo che la funzione contenga solo un’espressione, ad esempio una dichiarazione return semplice, come mostrato in un esempio pratico.

Un altro caso problematico riguarda l’uso delle stringhe template taggate (tagged template literals) nei template dei componenti. Queste, purtroppo, non sono supportate dalla compilazione Ahead-of-Time di Angular, causando errori di compilazione. Una soluzione alternativa è l’uso di funzioni ordinarie per creare dinamicamente parti dei template al momento della compilazione, evitando il bisogno di stringhe template avanzate.

L’adozione della compilazione AOT in Angular, sebbene estremamente vantaggiosa in termini di prestazioni e efficienza, richiede quindi attenzione a particolari limitazioni legate alla gestione delle dipendenze dinamiche, alla creazione di componenti in fase di runtime e alla dichiarazione dei metadati. Superare questi ostacoli richiede una conoscenza approfondita di come funziona la compilazione di Angular, nonché una certa adattabilità nei paradigmi di sviluppo.