Test Driven Development è una pratica di programmazione che è stata predicata e promossa da ogni comunità di sviluppatori sul pianeta. Eppure è una routine che è in gran parte trascurata da uno sviluppatore mentre apprende un nuovo framework. Scrivere i test delle unità sin dal primo giorno ti aiuterà a scrivere codice migliore, a individuare i bug con facilità ea mantenere un flusso di lavoro di sviluppo migliore.
Angular, essendo una vera e propria piattaforma di sviluppo front-end, ha il proprio set di strumenti per i test. Utilizzeremo i seguenti strumenti in questo tutorial:
('dovrebbe avere un componente definito', () => expect (component) .toBeDefined (););
Banco di prova
e ComponentFixtures
e funzioni di supporto come async
e fakeAsync
fanno parte del @ Angolare / core / testing
pacchetto. La conoscenza di queste utilità è necessaria se si desidera scrivere test che rivelano come i componenti interagiscono con il proprio modello, i propri servizi e altri componenti.In questo tutorial non copriremo i test funzionali usando Goniometro. Il goniometro è un popolare framework di test end-to-end che interagisce con l'interfaccia utente dell'applicazione utilizzando un browser effettivo.
In questo tutorial, siamo più preoccupati di testare i componenti e la logica del componente. Tuttavia, scriveremo un paio di test che dimostrano un'interfaccia utente di base usando il framework Jasmine.
L'obiettivo di questo tutorial è creare il front-end per un'applicazione Pastebin in un ambiente di sviluppo basato su test. In questo tutorial, seguiremo il popolare mantra TDD, che è "red / green / refactor". Scriveremo test che inizialmente falliscono (rosso) e quindi lavoriamo sul nostro codice applicativo per farli passare (verde). Rifonderemo il nostro codice quando inizierà a puzzare, il che significa che diventa gonfio e brutto.
Scriveremo i test per i componenti, i loro modelli, servizi e la classe Pastebin. L'immagine sotto illustra la struttura della nostra applicazione Pastebin. Gli elementi che vengono visualizzati in grigio verranno discussi nella seconda parte del tutorial.
Nella prima parte della serie ci concentreremo esclusivamente sull'impostazione dell'ambiente di test e sulla stesura di test di base per i componenti. Angolare è un framework basato sui componenti; pertanto, è una buona idea passare un po 'di tempo a familiarizzare con i test di scrittura dei componenti. Nella seconda parte della serie, scriveremo test più complessi per componenti, componenti con input, componenti indirizzati e servizi. Entro la fine della serie, avremo un'applicazione Pastebin completamente funzionante simile a questa.
Vista del componente PastebinVista del componente AddPasteIn questo tutorial, imparerai come:
L'intero codice per il tutorial è disponibile su Github.
https://github.com/blizzerand/pastebin-angular
Clona il repository e sentiti libero di controllare il codice se sei in dubbio in qualsiasi fase di questo tutorial. Iniziamo!
Gli sviluppatori di Angular hanno reso facile per noi impostare il nostro ambiente di test. Per iniziare, dobbiamo prima installare Angular. Preferisco usare l'Angular-CLI. È una soluzione all-in-one che si occupa di creare, generare, costruire e testare il tuo progetto Angular.
nuovo Pastebin
Ecco la struttura della directory creata da Angular-CLI.
Poiché i nostri interessi sono maggiormente orientati verso gli aspetti di testing in Angular, dobbiamo cercare due tipi di file.
karma.conf.js è il file di configurazione per il test runner Karma e l'unico file di configurazione di cui avremo bisogno per scrivere i test unitari in Angular. Per impostazione predefinita, Chrome è il browser-launcher predefinito utilizzato da Karma per acquisire i test. Creeremo un launcher personalizzato per l'esecuzione di Chrome senza testa e lo aggiungeremo al i browser
schieramento.
/*karma.conf.js*/ browser: ['Chrome', 'ChromeNoSandboxHeadless'], customLaunchers: ChromeNoSandboxHeadless: base: 'Chrome', flag: ['--no-sandbox', // Vedi https: / /chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md '--headless', '--disable-gpu', // Senza una porta di debug remota, Google Chrome si chiude immediatamente. '--remote-debugging-port = 9222',],,,
L'altro tipo di file che dobbiamo cercare è tutto ciò che finisce con .spec.ts
. Per convenzione, i test scritti in Jasmine sono chiamati specifiche. Tutte le specifiche di test dovrebbero essere situate all'interno dell'applicazione src / app /
directory perché è lì che Karma cerca le specifiche del test. Se si crea un nuovo componente o un servizio, è importante inserire le specifiche di test nella stessa directory in cui risiede il codice per il componente o il servizio.
Il nuovo
il comando ha creato un app.component.spec.ts
file per il nostro app.component.ts
. Sentiti libero di aprirlo e dare un'occhiata ai test sui gelsomini di Angular. Anche se il codice non ha alcun senso, va bene. Manterremo AppComponent come è per ora e lo useremo per ospitare i percorsi in qualche momento successivo nel tutorial.
Abbiamo bisogno di una classe Pastebin per modellare il nostro Pastebin all'interno dei componenti e dei test. Puoi crearne uno usando la Angular-CLI.
ng genera classe Pastebin
Aggiungi la seguente logica a Pastebin.ts:
export class Pastebin id: number; titolo: stringa; language: string; incolla: stringa; costruttore (valori: Object = ) Object.assign (this, values); export const Languages = ["Ruby", "Java", "JavaScript", "C", "Cpp"];
Abbiamo definito una classe Pastebin e ogni istanza di questa classe avrà le seguenti proprietà:
id
titolo
linguaggio
incolla
Crea un altro file chiamato pastebin.spec.ts
per la suite di test.
/ * pastebin.spec.ts * / // importa l'importazione della classe Pastebin Pastebin da './pastebin'; describe ('Pastebin', () => it ('dovrebbe creare un'istanza di Pastebin', () => expect (new Pastebin ()). toBeTruthy (););)
La suite di test inizia con a descrivere
blocco, che è una funzione globale del gelsomino che accetta due parametri. Il primo parametro è il titolo della suite di test e il secondo è la sua effettiva implementazione. Le specifiche sono definite utilizzando un esso
funzione che prende due parametri, simili a quelli del descrivere
bloccare.
Specifiche multiple (esso
blocchi) possono essere annidati all'interno di una suite di test (descrivere
bloccare). Tuttavia, assicurati che i titoli della suite di test siano nominati in modo tale che siano non ambigui e più leggibili perché servono come documentazione per il lettore.
Aspettative, implementate usando il aspettarsi
funzione, vengono utilizzati da Jasmine per determinare se una specifica deve passare o fallire. Il aspettarsi
la funzione accetta un parametro noto come valore effettivo. Viene quindi incatenato con un'altra funzione che prende il valore previsto. Queste funzioni sono chiamate funzioni di matcher e useremo le funzioni di matcher come toBeTruthy ()
, da definire()
, essere()
, e contenere()
molto in questo tutorial.
expect (new Pastebin ()). toBeTruthy ();
Quindi con questo codice, abbiamo creato una nuova istanza della classe Pastebin e ci aspettiamo che sia vera. Aggiungiamo un'altra specifica per confermare che il modello di Pastebin funzioni come previsto.
('dovrebbe accettare valori', () => lasciare pastebin = new Pastebin (); pastebin = id: 111, titolo: "Hello world", lingua: "Ruby", incolla: 'print "Hello"', expect (pastebin.id) .toEqual (111); expect (pastebin.language) .toEqual ("Ruby"); expect (pastebin.paste) .toEqual ('print "Hello"';;));
Abbiamo istanziato la classe Pastebin e aggiunto alcune aspettative alle nostre specifiche di prova. Correre ng test
per verificare che tutti i test siano verdi.
Generare un servizio usando il comando seguente.
ng genera un servizio pastebin
PastebinService
ospiterà la logica per l'invio di richieste HTTP al server; tuttavia, non abbiamo un'API del server per l'applicazione che stiamo costruendo. Pertanto, simuleremo la comunicazione del server utilizzando un modulo noto come InMemoryWebApiModule.
Installare angolare-in-memoria-web-api
via npm:
npm installa angular-in-memory-web-api --save
Aggiorna AppModule con questa versione.
/ * app.module.ts * / import BrowserModule da '@ angular / platform-browser'; import NgModule da '@ angular / core'; // Componenti import AppComponent da './app.component'; // Servizio per Pastebin import PastebinService da "./pastebin.service"; // I moduli usati in questo tutorial importano HttpModule da '@ angular / http'; // In memoria Web api per simulare l'importazione di un server http InMemoryWebApiModule da 'angular-in-memory-web-api'; import InMemoryDataService da './in-memory-data.service'; @NgModule (dichiarazioni: [AppComponent,], importa: [BrowserModule, HttpModule, InMemoryWebApiModule.forRoot (InMemoryDataService),], provider: [PastebinService], bootstrap: [AppComponent]) classe di esportazione AppModule
Creare un InMemoryDataService
che implementa InMemoryDbService
.
/*in-memory-data.service.ts*/ import InMemoryDbService da 'angular-in-memory-web-api'; importare Pastebin da "./pastebin"; classe di esportazione InMemoryDataService implementa InMemoryDbService createDb () const pastebin: Pastebin [] = [id: 0, titolo: "Hello world Ruby", lingua: "Ruby", incolla: 'puts "Hello World"', id : 1, titolo: "Hello world C", lingua: "C", incolla: 'printf ("Hello world");', id: 2, titolo: "Hello world CPP", lingua: "C ++", pasta: 'cout<<"Hello world";', id: 3, title: "Hello world Javascript", language: "JavaScript", paste: 'console.log("Hello world")' ]; return pastebin;
Qui, pastebin
è una matrice di paste campione che verrà restituita o aggiornata quando eseguiamo un'azione HTTP come http.get
o http.post
.
/*pastebin.service.ts * / import Injectable da '@ angular / core'; importare Pastebin da "./pastebin"; import Http, Headers da '@ angular / http'; importare 'rxjs / add / operator / toPromise'; @Injectable () export class PastebinService // Il progetto utilizza InMemoryWebApi per gestire l'API del server. // Qui "api / pastebin" simula un URL dell'API del server private pastebinUrl = "api / pastebin"; intestazioni private = nuove intestazioni ('Content-Type': "application / json"); costruttore (http: Http privato) // getPastebin () esegue http.get () e restituisce una promessa pubblica getPastebin (): Promisereturn this.http.get (this.pastebinUrl) .toPromise () .then (response => response.json (). data) .catch (this.handleError); handleError privato (errore: qualsiasi): Promise console.error ('Si è verificato un errore', errore); return Promise.reject (error.message || error);
Il getPastebin ()
metodo effettua una richiesta HTTP.get e restituisce una promessa che si risolve in una matrice di oggetti Pastebin restituiti dal server.
Se ottieni un Nessun provider per HTTP errore mentre si esegue una specifica, è necessario importare il modulo HTTP nel file delle specifiche interessato.
I componenti sono il componente base di un'interfaccia utente in un'applicazione angolare. Un'applicazione angolare è un albero di componenti angolari.
- Documentazione angolare
Come evidenziato in precedenza nella sezione Panoramica, lavoreremo su due componenti in questo tutorial: PastebinComponent
e AddPasteComponent
. Il componente Pastebin è costituito da una struttura di tabella che elenca tutte le paste recuperate dal server. Il componente AddPaste mantiene la logica per la creazione di nuove paste.
Vai avanti e genera i componenti usando Angular-CLI.
ng g component --spec = false Pastebin
Il --spec = false
opzione dice al ANGular-CLI di non creare un file spec. Questo perché vogliamo scrivere unit test per i componenti da zero. Creare un pastebin.component.spec.ts
file all'interno del pastebin componenti cartella.
Ecco il codice per pastebin.component.spec.ts
.
import TestBed, ComponentFixture, async da '@ angular / core / testing'; import DebugElement da '@ angular / core'; importare PastebinComponent da "./pastebin.component"; importare By da '@ angular / platform-browser'; import Pastebin, Languages da '... / pastebin'; // Moduli utilizzati per testare l'importazione di HttpModule da '@ angular / http'; Descrivi ('PastebinComponent', () => // Dichiarazioni del dattiloscritto. lascia comp: PastebinComponent; lascia fixture: ComponentFixture; let de: DebugElement; let element: HTMLElement; lascia che mockPaste: Pastebin []; // beforeEach viene chiamato una volta prima di ogni blocco "it" in un test. // Usa questo per configurare il componente, inietti servizi ecc. BeforeEach (() => TestBed.configureTestingModule (dichiarazioni: [PastebinComponent], // dichiara il componente di prova importa: [HttpModule],); fixture = TestBed .createComponent (PastebinComponent); comp = fixture.componentInstance; de = fixture.debugElement.query (By.css ('. pastebin')); element = de.nativeElement;); )
C'è molto da fare qui. Smettiamola e prendiamo un pezzo alla volta. All'interno del descrivere
blocco, abbiamo dichiarato alcune variabili, e quindi abbiamo usato a beforeeach
funzione. beforeeach ()
è una funzione globale fornita da Jasmine e, come suggerisce il nome, viene richiamata una volta prima di ogni specifica in descrivere
blocco in cui è chiamato.
TestBed.configureTestingModule (dichiarazioni: [PastebinComponent], // dichiara il componente di prova importa: [HttpModule],);
Banco di prova
class fa parte delle utility di testing Angular e crea un modulo di test simile a quello di @NgModule
classe. Inoltre, puoi configurare Banco di prova
usando il configureTestingModule
metodo. Ad esempio, è possibile creare un ambiente di test per il progetto che emula l'effettiva applicazione Angolare, quindi è possibile estrarre un componente dal modulo dell'applicazione e ricollegarlo a questo modulo di test.
fixture = TestBed.createComponent (PastebinComponent); comp = fixture.componentInstance; de = fixture.debugElement.query (By.css ('div')); element = de.nativeElement;
Dalla documentazione Angular:
IlcreateComponent
metodo restituisce aComponentFixture
, una maniglia sull'ambiente di test che circonda il componente creato. L'apparecchiatura fornisce accesso all'istanza del componente stesso e alDebugElement
, che è un handle sull'elemento DOM del componente.
Come accennato in precedenza, abbiamo creato un appuntamento fisso del PastebinComponent
e quindi ha usato quella apparecchiatura per creare un'istanza del componente. Ora possiamo accedere alle proprietà e ai metodi del componente all'interno dei nostri test chiamando comp.property_name
. Dal momento che il dispositivo fornisce anche l'accesso al debugElement
, ora possiamo interrogare gli elementi DOM e i selettori.
C'è un problema con il nostro codice che non abbiamo ancora pensato. Il nostro componente ha un modello esterno e un file CSS. Il recupero e la lettura dal file system è un'attività asincrona, a differenza del resto del codice, che è tutto sincrono.
Angular ti offre una funzione chiamata async ()
che si prende cura di tutte le cose asincrone. Ciò che async fa è tenere traccia di tutte le attività asincrone al suo interno, nascondendo al contempo la complessità dell'esecuzione asincrona. Quindi avremo ora due funzioni beforeEach, una asincrona beforeeach ()
e un sincrono beforeeach ()
.
/ * pastebin.component.spec.ts * / // beforeEach viene chiamato una volta prima di ogni blocco "it" in un test. // Usa questo per configurare il componente, inietti servizi ecc. Prima di Each (async (() => // async prima viene usato per compilare template esterni che è qualsiasi attività asincrona TestBed.configureTestingModule (dichiarazioni: [PastebinComponent], / / dichiara le importazioni del componente di prova: [HttpModule],) .compileComponents (); // compile template e css)); beforeEach (() => // Ed ecco la fixture sincrona della funzione asincrona = TestBed.createComponent (PastebinComponent); comp = fixture.componentInstance; de = fixture.debugElement.query (By.css ('. pastebin')); element = de.nativeElement;);
Non abbiamo ancora scritto alcuna specifica di prova. Tuttavia, è una buona idea creare in anticipo un profilo delle specifiche. L'immagine sotto mostra un disegno approssimativo del componente Pastebin.
Abbiamo bisogno di scrivere test con le seguenti aspettative.
pastebinService
è iniettato nel componente e i suoi metodi sono accessibili.OnInit ()
è chiamato.I primi tre test sono facili da implementare.
('dovrebbe avere un Component', () => expect (comp) .toBeTruthy ();); ('dovrebbe avere un titolo', () => comp.title = 'Pastebin Application'; fixture.detectChanges (); expect (element.textContent) .toContain (comp.title);) it ('dovrebbe avere una tabella per visualizzare le paste ', () => expect (element.innerHTML) .toContain ("thead"); expect (element.innerHTML) .toContain ("tbody");)
In un ambiente di test, Angular non associa automaticamente le proprietà del componente con gli elementi del modello. Devi chiamare esplicitamente fixture.detectChanges ()
ogni volta che si desidera associare una proprietà del componente al modello. L'esecuzione del test dovrebbe darti un errore perché non abbiamo ancora dichiarato la proprietà del titolo all'interno del nostro componente.
title: string = "Applicazione Pastebin";
Non dimenticare di aggiornare il modello con una struttura di base della tabella.
titolo
id Titolo linguaggio Codice
Per il resto dei casi, dobbiamo iniettare il Pastebinservice
e scrivere test che si occupano dell'interazione component-service. Un servizio reale potrebbe effettuare chiamate a un server remoto e iniettarlo nella sua forma grezza sarà un compito laborioso e impegnativo.
Invece, dovremmo scrivere test che si concentrino sul fatto che il componente interagisca con il servizio come previsto. Aggiungeremo le specifiche che spiano il pastebinService
e la sua getPastebin ()
metodo.
Innanzitutto, importa il PastebinService
nella nostra suite di test.
import PastebinService da '... /pastebin.service';
Quindi, aggiungilo al fornitori
array all'interno TestBed.configureTestingModule ()
.
TestBed.configureTestingModule (dichiarazioni: [CreateSnippetComponent], provider: [PastebinService],);
Il codice seguente crea una spia Jasmine progettata per tracciare tutte le chiamate verso il getPastebin ()
metodo e restituire una promessa che risolve immediatamente mockPaste
.
// Il vero PastebinService è iniettato nel componente let pastebinService = fixture.debugElement.injector.get (PastebinService); mockPaste = [id: 1, titolo: "Hello world", lingua: "Ruby", incolla: "puts 'Hello'"]; spy = spyOn (pastebinService, 'getPastebin') .and.returnValue (Promise.resolve (mockPaste));
La spia non è preoccupata per i dettagli di implementazione del servizio reale, ma, al contrario, ignora qualsiasi chiamata al reale getPastebin ()
metodo. Inoltre, tutte le chiamate remote sono sepolte all'interno getPastebin ()
sono ignorati dai nostri test. Nella seconda parte del tutorial scriveremo test unitari isolati per servizi angolari.
Aggiungi i seguenti test a pastebin.component.spec.ts
.
('non dovrebbe mostrare il pastebin prima di OnInit', () => this.tbody = element.querySelector ("tbody"); // Prova questo senza il metodo 'replace (\ s \ s + / g,') ' e vedi cosa succede (this.tbody.innerText.replace (/ \ s \ s + / g, ")). toBe (" "," tbody dovrebbe essere vuoto "); expect (spy.calls.any ()). toBe (falso, "Spy non dovrebbe essere ancora chiamato");); ('non dovrebbe ancora mostrare pastebin dopo il componente inizializzato', () => fixture.detectChanges (); // il servizio getPastebin è asincrono, ma il test no. expect (this.tbody.innerText.replace (/ \ s \ s + / g, ")). toBe (" ", 'tbody dovrebbe essere ancora vuoto'); expect (spy.calls.any ()). toBe (true, 'getPastebin dovrebbe essere chiamato');); ('dovrebbe mostrare il pastebin dopo che getPastebin promise resolves', async () => fixture.detectChanges (); fixture.whenStable (). then (() => fixture.detectChanges (); expect (comp.pastebin). toEqual (jasmine.objectContaining (mockPaste)); expect (element.innerText.replace (/ \ s \ s + / g, ")). toContain (mockPaste [0] .title););)
I primi due test sono i test sincroni. La prima specifica controlla se il innerText
del div
l'elemento rimane vuoto finché il componente non è inizializzato. Il secondo argomento della funzione matcher di Jasmine è facoltativo e viene visualizzato quando il test fallisce. Questo è utile quando si hanno più dichiarazioni di aspettativa all'interno di una specifica.
Nella seconda specifica, il componente è inizializzato (perché fixture.detectChanges ()
viene chiamato) e anche la spia viene richiamata, ma il modello non deve essere aggiornato. Anche se la spia restituisce una promessa risolta, il mockPaste
non è ancora disponibile Non dovrebbe essere disponibile a meno che il test non sia un test asincrono.
Il terzo test utilizza un async ()
funzione discussa in precedenza per eseguire il test in una zona di prova asincrona. async ()
è usato per fare un test sincrono asincrono. fixture.whenStable ()
viene chiamato quando tutte le attività asincrone pendenti sono completate e quindi un secondo round di fixture.detectChanges ()
viene chiamato per aggiornare il DOM con i nuovi valori. L'aspettativa nel test finale assicura che il nostro DOM sia aggiornato con il mockPaste
valori.
Per far passare i test, dobbiamo aggiornare il nostro pastebin.component.ts
con il seguente codice.
/*pastebin.component.ts*/ import Component, OnInit da '@ angular / core'; importare Pastebin da '... / pastebin'; import PastebinService da '... /pastebin.service'; @Component (selector: 'app-pastebin', templateUrl: './pastebin.component.html', styleUrls: ['./pastebin.component.css']) classe di esportazione PastebinComponent implementa OnInit title: string = " Applicazione Pastebin "; pastebin: any = []; costruttore (pubblico pastebinServ: PastebinService) // loadPastebin () viene chiamato su init ngOnInit () this.loadPastebin (); public loadPastebin () // richiama il metodo getPastebin () del servizio pastebin e memorizza la risposta nella proprietà 'pastebin' this.pastebinServ.getPastebin (). then (pastebin => this.pastebin = pastebin);
Anche il modello deve essere aggiornato.
titolo
id Titolo linguaggio Codice Paste.id Paste.title Paste.language Visualizza il codice
Generare un componente AddPaste utilizzando Angular-CLI. L'immagine sotto mostra il design del componente AddPaste.
La logica del componente dovrebbe superare le seguenti specifiche.
ShowModal
proprietà a vero
. (ShowModal
è una proprietà booleana che diventa true quando viene visualizzata la modale e false quando la modale è chiusa.)addPaste ()
metodo.ShowModal
proprietà a falso
.Abbiamo elaborato i primi tre test per te. Vedi se riesci a far passare i test da solo.
descrivere ('AddPasteComponent', () => lasciare componente: AddPasteComponent; lasciare fixture: ComponentFixture; let de: DebugElement; let element: HTMLElement; lascia spiare: jasmine.Spy; let pastebinService: PastebinService; beforeEach (async (() => TestBed.configureTestingModule (dichiarazioni: [AddPasteComponent], importa: [HttpModule, FormsModule], provider: [PastebinService],) .compileComponents ();)); beforeEach (() => // fix di inizializzazione = TestBed.createComponent (AddPasteComponent); pastebinService = fixture.debugElement.injector.get (PastebinService); component = fixture.componentInstance; de = fixture.debugElement.query (By.css ( '.add-paste')); element = de.nativeElement; spy = spyOn (pastebinService, 'addPaste'). and.callThrough (); // chiede a fixture di rilevare le modifiche fixture.detectChanges ();); it ('dovrebbe essere creato', () => expect (component) .toBeTruthy ();); ('dovrebbe mostrare il pulsante' create Paste '', () => // Ci dovrebbe essere un pulsante create nel template expect (element.innerText) .toContain ("create Paste");); ('non dovrebbe visualizzare la modale a meno che il pulsante non venga cliccato', () => // source-model è un id per la modale. Non dovrebbe apparire se non si fa clic sul pulsante di creazione expect (element.innerHTML). not.toContain ("source-modal");) it ("dovrebbe visualizzare la modale quando viene cliccato 'create Paste'", () => lasciare createPasteButton = fixture.debugElement.query (By.css ("button" //) triggerEventHandler simula un evento click sull'oggetto button createPasteButton.triggerEventHandler ("click", null); fixture.detectChanges (); expect (element.innerHTML) .toContain ("source-modal"); expect (componente .showModal) .toBeTruthy ("showModal dovrebbe essere true");))
DebugElement.triggerEventHandler ()
è l'unica cosa nuova qui. Viene utilizzato per attivare un evento click sull'elemento button su cui viene chiamato. Il secondo parametro è l'oggetto evento e lo abbiamo lasciato vuoto da quello del componente clic()
non si aspetta uno.
Questo è tutto per il giorno. In questo primo articolo, abbiamo imparato:
Nel prossimo tutorial creeremo nuovi componenti, scriveremo più componenti di test con input e output, servizi e percorsi. Restate sintonizzati per la seconda parte della serie. Condividi i tuoi pensieri attraverso i commenti.