Istruzioni per testare in modo efficiente le soluzioni di Azure

Aggiornamento: gennaio 2015

Autore: Suren Machiraju

Revisori: Jaime Alva Bravo e Steve Wilkins

Dopo aver progettato, scritto il codice e distribuito la propria soluzione di Microsoft Azure, a volte si scopre che non funziona. Questo articolo fornisce istruzioni su come testare le applicazioni di Microsoft Azure durante l'intero ciclo di sviluppo del software. Come ambito sono inclusi la logica di business e i test completi degli scenari end-to-end. Questo articolo illustra come effettuare le seguenti operazioni:

  • Sviluppare unit test per i componenti della logica di business eliminando le dipendenze dai componenti di Microsoft Azure.

  • Progettare test end-to-end di integrazione.

  • Eliminare il sovraccarico per l'installazione, l'inizializzazione, la pulizia e la disinstallazione delle risorse del servizio di Microsoft Azure, ad esempio le code, per ogni esecuzione dei test.

  • Eliminare la creazione di infrastrutture duplicate per gli spazi dei nomi ACS, le code di Service Bus e altre risorse.

Questo articolo inoltre fornisce una panoramica delle diverse tecnologie e tecniche disponibili per eseguire il testing delle applicazioni di Microsoft Azure.

In questo articolo le seguenti modifiche rappresentano una novità:

  1. Viene usato Visual Studio 2013.

  2. Microsoft Fakes in Visual Studio 2013 sostituisce i mole. Questa funzionalità è descritta nell'articolo "Isolamento del codice sottoposto a test con Microsoft Fakes".

  3. In Visual Studio 2013 è ora disponibile una nuova versione di code coverage che viene richiamata direttamente da Esplora test. Per una descrizione, leggere l'articolo "Uso di code coverage per determinare la quantità di codice testato".

  4. Il team Pex ora ha rilasciato una versione leggera di Pex denominata Code Digger. Per una descrizione, vedere l'articolo "Microsoft Code Digger".

  5. A partire da Visual Studio 2012, non è più consigliabile eseguire il testing dei metodi privati. Per una spiegazione, leggere il post relativo all'esecuzione di unit test per i metodi privati.

Sono disponibili due tipi di test:

  • Gli unit test sono test molto mirati che verificano il comportamento di un'unica funzione specifica. Questi test vengono indicati come "codice sotto test" o CUT (Code Under Test). È necessario rimuovere tutte le eventuali dipendenze richieste da tale codice.

  • I test di integrazione sono test di portata più ampia che verificano il comportamento di più parti di funzionalità contemporaneamente. In molti casi sono simili agli unit test, con la differenza che coprono diverse aree funzionali e includono varie dipendenze.

Complessivamente, questi test sono incentrati sulla creazione e l'uso di elementi sostitutivi noti come test double. I test double usati sono i seguenti:

  • I fake sono oggetti simulati che implementano la stessa interfaccia dell'oggetto che rappresentano. I fake restituiscono risposte predefinite e ognuno di essi contiene un set di stub di metodo e funge da sostituto di quanto compilato a livello di programmazione.

  • Gli stub simulano il comportamento degli oggetti software.

  • Gli shim consentono di isolare il proprio codice dagli assembly che non fanno parte della propria soluzione. Isolano inoltre i componenti della soluzione l'uno dall'altro.

Una volta eseguiti, questi test possono verificare lo stato e il comportamento. Un esempio di stato è il risultato della chiamata di un metodo e la restituzione di un valore specifico. Un esempio di comportamento invece è la chiamata di un metodo in un determinato ordine o per un determinato numero di volte.

Uno degli scopi principali dell'esecuzione di unit test è eliminare le dipendenze. Per il framework di Azure, queste dipendenze includono quanto segue:

  • Code di Service Bus

  • Servizio di controllo di accesso

  • Cache

  • Tabelle, BLOB e code di Azure

  • Database SQL di Azure

  • Unità Azure (nota in precedenza come unità cloud)

  • Altri servizi Web

Quando si compilano i test per le applicazioni di Azure, queste dipendenze vengono sostituite in modo che i test possano esclusivamente verificare il comportamento della logica.

Gli esempi relativi alle code di Service Bus, inclusi gli strumenti e le tecniche, illustrati in questo articolo si applicano anche a tutte le altre dipendenze.

Per implementare il framework di testing per le proprie applicazioni di Microsoft Azure, è necessario quanto segue:

  • Un framework di esecuzione degli unit test per definire ed eseguire i propri test

  • Un framework di creazione di versioni fittizie per facilitare l'isolamento delle dipendenze e la creazione di unit test con ambito limitato

  • Strumenti che facilitino la generazione automatica di unit test per garantire maggiore code coverage

  • Altri framework utili per progettazioni testabili mediante l'inserimento di dipendenze (DI, Dependency Injection) e l'applicazione del modello Inversione di controllo (IoC, Inversion of Control).

Visual Studio include un'utilità da riga di comando, denominata MSTest, per eseguire gli unit test creati in tale ambiente. Visual Studio inoltre include una serie di modelli di progetto e di elemento per supportare il testing. In genere si crea un progetto di test e quindi si aggiungono le classi, note come test fixture, con associato l'attributo [TestClass]. Le classi contengono metodi a cui è associato l'attributo [TestMethod]. All'interno di MSTest diverse finestre di Visual Studio consentono di eseguire unit test definiti nel progetto. Dopo l'esecuzione, è anche possibile esaminarne i risultati.

noteNota
Le edizioni Express, Professional e Test Professional di Visual Studio 2013 non contengono MSTest.

In MSTest gli unit test seguono il modello AAA (Arrange-Act-Assert).

  • Arrange: creare tutti gli oggetti, prerequisiti, le configurazioni e tutte le altre precondizioni necessarie e gli input richiesti dal codice sotto test.

  • Act: eseguire l'effettivo test con ambito limitato sul codice.

  • Assert: verificare che i risultati siano quelli previsti.

Le librerie del framework di MSTest includono le classi helper PrivateObject e PrivateType. Queste classi usano la reflection per facilitare il richiamo di membri di istanza non pubblici o membri statici dal codice degli unit test.

Le edizioni Premium e Ultimate di Visual Studio includono strumenti per unit test avanzati. Questi strumenti si integrano con MSTest e consentono anche di analizzare la quantità di codice di cui gli unit test verificano il comportamento. Essi inoltre identificano con un colore il codice sorgente per indicare la copertura. Questa funzionalità è denominata code coverage.

Uno degli obiettivi degli unit test è eseguire il testing in isolamento. Tuttavia, è spesso impossibile verificare il codice sotto test in isolamento. A volte il codice non è scritto in modo che sia verificabile. La riscrittura del codice sarebbe un'attività complessa perché si basa su altre librerie non isolabili facilmente. È questo ad esempio il caso del codice che interagisce con ambienti esterni. L'uso di un framework di creazione di versioni fittizie semplifica l'isolamento di entrambi i tipi di dipendenze.

Nella sezione relativa ai collegamenti alla fine di questo articolo è disponibile un elenco di tali framework da prendere in considerazione. Questo articolo tratta specificamente l'uso di Microsoft Fakes.

Microsoft Fakes consente di isolare il codice da testare sostituendo altre parti dell'applicazione con stub o shim. Si tratta di piccoli frammenti di codice sotto il controllo dei test. Se si isola il proprio codice per il testing e il test ha esito negativo, si determina con certezza che il problema dipende solo ed esclusivamente dal codice. Gli stub e gli shim consentono inoltre di testare il codice anche se altre parti dell'applicazione ancora non funzionano.

I fake sono disponibili in due forme:

  • Uno stub sostituisce una classe con un sostituto di piccole dimensioni che implementa la stessa interfaccia. Per usare gli stub è necessario progettare l'applicazione in modo che ogni componente dipenda solo dalle interfacce e non da altri componenti. Per "componente" si intende una classe o un gruppo di classi che vengono progettate e aggiornate insieme e che in genere sono contenute in un assembly.

  • Uno shim modifica il codice compilato dell'applicazione in fase di runtime. Invece di effettuare una determinata chiamata a un metodo, l'applicazione esegue il codice shim fornito dal test. È possibile usare gli shim per sostituire le chiamate agli assembly non modificabili, ad esempio agli assembly .NET.

Code Digger analizza i possibili percorsi di esecuzione attraverso il codice .NET. Come risultato viene generata una tabella in cui ogni riga mostra un comportamento univoco del codice. Questa tabella è utile per comprendere il comportamento del codice e anche per individuare bug nascosti.

Per analizzare il proprio codice nell'editor di Visual Studio, scegliere la nuova voce digenerazione della tabella degli input/output dal menu di scelta rapida per richiamare Code Digger. Quest'ultimo calcola e visualizza le coppie di input/output. Code Digger ricerca sistematicamente i bug, le eccezioni e gli errori di asserzione.

Per impostazione predefinita, Code Digger funziona solo con codice .NET pubblico residente in librerie di classi portabili. Più avanti in questo articolo verrà illustrato come configurare Code Digger per esplorare altri progetti .NET.

Code Digger usa il motore Pex e il risolutore di vincoli Z3 di Microsoft Research per analizzare sistematicamente tutti i rami del codice. Code Digger tenta di generare un gruppo di test in grado di raggiungere un elevato grado di code coverage.

È possibile usare Microsoft Unity per avere contenitori estendibili per l'inserimento di dipendenze (DI) e l'inversione di controllo (IoC). Esso supporta l'intercettazione, nonché l'inserimento di costruttori, proprietà e chiamate ai metodi. Microsoft Unity e gli strumenti analoghi facilitano la creazione di progettazioni testabili che consentano di inserire le proprie dipendenze a tutti i livelli dell'applicazione. Si presuppone che l'applicazione sia stata progettata e compilata in prospettiva dell'inserimento di dipendenze e di uno di questi framework.

Tali framework sono utili per scrivere codice verificabile e, di conseguenza, codice funzionante. Possono tuttavia prevedere elevati requisiti di progettazione a monte. I contenitori DI e IoC non vengono trattati in questo articolo.

In questa sezione viene illustrata una soluzione che include un sito Web ospitato in un ruolo Web. Il sito Web effettua il push dei messaggi a una coda, quindi un ruolo di lavoro elabora i messaggi dalla coda. Si intende testare alcuni aspetti relativi a tutti e tre questi elementi.

Si supponga di avere un sito Web che crea ordini e una coda di Service Bus che crea una coda per l'elaborazione di tali ordini. La pagina Web è simile a quella illustrata nella figura 1:

Figura 1

Figura 1

Quando l'utente fa clic su Create, la coda di Service Bus inserisce il nuovo ordine nell'azione Create sul controller associato. L'azione viene implementata come segue:

noteNota
La classe MicrosoftAzureQueue è una classe wrapper che usa le API .NET di Service Bus, ad esempio MessageSender e MessageReceiver, per interagire con le code di Service Bus.

private IMicrosoftAzureQueue queue;
public OrderController()
{
    queue = new MicrosoftAzureQueue();
}
public OrderController(IMicrosoftAzureQueue queue)
{
    this.queue = queue;
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "OrderId,Description")] Order order)
{
    try
    {
        if (ModelState.IsValid)
        {
            string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
            string queueName = "ProcessingQueue";
            queue.InitializeFromConnectionString(connectionString, queueName);
            queue.Send(order);
        }
        return View("OrderCreated");
    }
    catch (Exception ex)
    {
        Trace.TraceError(ex.Message);
        return View("Error");
    }            
}

Notare che il codice recupera le impostazioni di configurazione da CloudConfigurationManager e invia alla coda un messaggio contenente l'ordine. Notare anche che l'azione Create usa i seguenti metodi:

  • InitializeFromConnectionString (stringa ConnectionString, stringa QueueName)

  • Send (classe MicrosoftAzureQueue)

Si desidera creare deviazioni per questi metodi usando Fakes per controllare il relativo comportamento e rimuovere le dipendenze dall'ambiente reale. Mediante Fakes, non è più necessario eseguire i test nell'emulatore di Azure o chiamare la coda di Service Bus. Il comportamento dell'azione Create viene verificato sul controller per accertarsi che l'input dell'ordine sia quello inviato alla coda. Il metodo Send verifica che l'input contenga l'ID e la descrizione dell'ordine specificati per l'azione. Verifica quindi che come risultato venga visualizzata la vista OrderCreated.

Grazie a Fakes, è facile creare un unit test per l'implementazione sopra descritta. All'interno del progetto di test fare clic con il pulsante destro del mouse sull'assembly contenente i tipi di cui si desidera creare versioni fittizie, quindi scegliere Aggiungi assembly Fakes.

Nell'esempio si fa clic con il pulsante destro del mouse su Microsoft.ServiceBus e si sceglie Aggiungi assembly Fakes. Un file XML denominato "Microsoft.ServiceBus.Fakes" viene aggiunto al progetto di test. Ripetere l'azione per l'assembly Microsoft.WindowsAzure.Configuration.

Quando si compila il progetto di test, vengono aggiunti riferimenti alle versioni fittizie generate automaticamente per questi assembly. Gli assembly generati nell'esempio sono “Microsoft.ServiceBus.Fakes” e “Microsoft.WindowsAzure.Configuration”.

Creare un metodo per lo unit test e applicare l'attributo [TestCategory("With fakes")]. All'interno dello unit test vengono usati shim per isolare le diverse parti dell'applicazione.

Uso di shim per isolare l'applicazione da altri assembly per l'esecuzione di unit test

Gli shim sono una delle due tecnologie fornite dal framework Microsoft Fakes che consentono di isolare facilmente i componenti sottoposti a test dall'ambiente. Gli shim deviano le chiamate a metodi specifici verso il codice scritto come parte del test. Molti metodi restituiscono risultati diversi in base alle condizioni esterne. Uno shim tuttavia è controllato dal test e può restituire risultati coerenti a ogni chiamata. Questo semplifica considerevolmente la scrittura dei test. Usare gli shim per isolare il proprio codice dagli assembly che non fanno parte della propria soluzione. Per isolare tra loro i diversi componenti della soluzione, è consigliabile usare gli stub.

Uso di stub per isolare tra loro le diverse parti dell'applicazione per l'esecuzione dello unit test

Gli stub sono una delle due tecnologie fornite dal framework Microsoft Fakes che consentono di isolare facilmente un componente da testare dagli altri componenti che chiama. Uno stub è un piccolo frammento di codice che prende il posto di un altro componente durante il testing. L'uso di uno stub ha come vantaggio la restituzione di risultati coerenti, semplificando la scrittura del test. Inoltre, è possibile eseguire i test anche se gli altri componenti ancora non funzionano.

In questo test case vengono usati shim per CloudConfigurationManager e BrokeredMessage dagli assembly di Azure. Gli stub vengono invece usati per MicrosoftAzureQueue, che è una classe della soluzione.

[TestMethod]
[TestCategory("With fakes")]
public void Test_Home_CreateOrder()
{
    // Shims can be used only in a ShimsContext
    using (ShimsContext.Create())
    {
        // Arrange
        // Use shim for CloudConfigurationManager.GetSetting
        Microsoft.WindowsAzure.Fakes.ShimCloudConfigurationManager.GetSettingString = (key) =>
        {
            return "mockedSettingValue";
        };
                
        // Create the fake queue:
        // In the completed application, queue would be a real one:
        bool wasCreateFromConnString = false;
        Order orderSent = null;
        IMicrosoftAzureQueue queue =
                new OrderWebRole.Queue.Fakes.StubIMicrosoftAzureQueue() // Generated by Fakes.
                {
                    // Define each method:
                    // Name is original name + parameter types:
                    InitializeFromConnectionStringStringString = (connectionString, queueName) => {
                        wasCreateFromConnString = true;
                    },
                    SendOrder = (order) => {
                    orderSent = order;
                    }
                };

        // Component under test
        OrderController controller = new OrderController(queue);

        // Act
        Order inputOrder = new Order()
        {
            OrderId = System.Guid.NewGuid(),
            Description = "A mock order"
        };
        ViewResult result = controller.Create(inputOrder) as ViewResult;

        //Assert
        Assert.IsTrue(wasCreateFromConnString);
        Assert.AreEqual("OrderCreated", result.ViewName);
        Assert.IsNotNull(orderSent);
        Assert.AreEqual(inputOrder.OrderId, orderSent.OrderId);
        Assert.AreEqual(inputOrder.Description, orderSent.Description);
    }
}

Il ruolo Web gestisce l'aggiunta di un ordine alla coda. Considerare ora il modo in cui testare un ruolo di lavoro che elabora gli ordini recuperandoli dalla coda di Service Bus. Il metodo Run nel ruolo di lavoro esegue periodicamente il polling della coda per individuare gli ordini ed elaborarli.

private IMicrosoftAzureQueue queue;
public WorkerRole()
{
    queue = new MicrosoftAzureQueue();
}

public WorkerRole(IMicrosoftAzureQueue queue)
{
    this.queue = queue;
}
public override void Run()
{
    try
    {
        string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
        string queueName = "ProcessingQueue";
               
        queue.InitializeFromConnectionString(connectionString, queueName);
            
        queue.CreateQueueIfNotExists();
              
        while (true)
        {
            Thread.Sleep(2000);
            //Retrieve order from Service Bus Queue  
            TryProcessOrder(queue);
        }
    }
    catch (Exception ex)
    {
        if (queue != null)
            queue.Close();
        System.Diagnostics.Trace.TraceError(ex.Message);
    }
}

Si desidera creare un test per verificare che la routine recuperi correttamente un messaggio. Di seguito è riportato lo unit test completo per il metodo Run del ruolo di lavoro.

[TestMethod]
public void Test_WorkerRole_Run()
{
    // Shims can be used only in a ShimsContext:
    using (ShimsContext.Create())
    {
        Microsoft.WindowsAzure.Fakes.ShimCloudConfigurationManager.GetSettingString = (key) =>
        {
            return "mockedSettingValue";
        };

        // Arrange 
        bool wasEnsureQueueExistsCalled = false;
        int numCallsToEnsureQueueExists = 0;

        // Create the fake queue:
        // In the completed application, queue would be a real one:
        bool wasConnectionClosedCalled = false;
        bool wasCreateFromConnString = false;
        bool wasReceiveCalled = false;
        int numCallsToReceive = 0;

        bool wasCompleteCalled = false;
        int numCallsToComplete = 0;
        IMicrosoftAzureQueue queue =
                new OrderWebRole.Queue.Fakes.StubIMicrosoftAzureQueue() // Generated by Fakes.
                {

                    // Define each method:
                    // Name is original name + parameter types:
                    InitializeFromConnectionStringStringString = (connectionString, queueName) =>
                    {
                        wasCreateFromConnString = true;
                    },
                    CreateQueueIfNotExists = () =>
                    {
                        wasEnsureQueueExistsCalled = true;
                        numCallsToEnsureQueueExists++;
                    },
                    Receive = () =>
                {
                    wasReceiveCalled = true;
                    if (numCallsToReceive >= 3) throw new Exception("Aborting Run");
                    numCallsToReceive++;
                    Order inputOrder = new Order()
                    {
                        OrderId = System.Guid.NewGuid(),
                        Description = "A mock order"
                    };
                    return new BrokeredMessage(inputOrder);
                },
                    Close = () =>
                    {
                        wasConnectionClosedCalled = true;
                    }

                };


        Microsoft.ServiceBus.Messaging.Fakes.ShimBrokeredMessage.AllInstances.Complete = (message) =>
        {
            wasCompleteCalled = true;
            numCallsToComplete++;
        };

        WorkerRole workerRole = new WorkerRole(queue);

        //Act
        workerRole.Run();

        //Assert
        Assert.IsTrue(wasCreateFromConnString);
        Assert.IsTrue(wasConnectionClosedCalled);
        Assert.IsTrue(wasEnsureQueueExistsCalled);
        Assert.IsTrue(wasReceiveCalled);
        Assert.AreEqual(1, numCallsToEnsureQueueExists);
        Assert.IsTrue(numCallsToReceive > 0);
        Assert.IsTrue(wasCompleteCalled);
        Assert.IsTrue(numCallsToComplete > 0);
        Assert.AreEqual(numCallsToReceive, numCallsToComplete);

    }
}

È necessario impostare il delegato della proprietà AllInstances del tipo Fake generato. Usando il delegato, qualsiasi istanza creata per il tipo Fake effettivo effettua la deviazione attraverso uno qualunque dei metodi per cui sono stati definiti delegati.

Nell'esempio si desidera usare il metodo Run dell'istanza originale, ma fornire deviazioni per i metodi CreateQueue e TryProcessOrder dell'istanza. Nel codice viene generata un'eccezione in modo da poter uscire dal ciclo infinito mantenuto dal metodo Run in un momento predeterminato.

Ci si potrebbe chiedere il motivo per cui non si usano direttamente MessageSender/MessageReceiver e le classi correlate di Service Bus SDK invece di inserire un tipo helper. Per isolare completamente il codice in modo che non chiami Service Bus del mondo reale, sono disponibili due opzioni:

  • Scrivere fake che ereditino dalle classi astratte nello spazio dei nomi Microsoft.ServiceBus.

  • Lasciare che Fakes crei tipi fittizi per tutto.

Entrambi questi approcci sono complessi e in tutti e due i casi alla fine è necessaria la reflection in classi come TokenProvider e QueueClient. La reflection causa i seguenti problemi:

  • Da questi tipi astratti è necessario creare tipi derivati che espongono tutte le relative sostituzioni obbligatorie.

  • È necessario esporre i tipi interni su cui si basano effettivamente le versioni reali di queste classi.

  • Per i tipi interni, è necessario ricreare i relativi costruttori o metodi factory in modo intelligente per verificare il comportamento della dipendenza da Service Bus reale.

L'opzione migliore è quella di inserire un proprio tipo helper. Non serve altro per creare una versione fittizia, deviare e isolare il codice da Service Bus dell'ambiente reale.

Per analizzare gli elementi verificati da questi unit test, è possibile esaminare i dati di code coverage. Se si eseguono entrambi gli unit test mediante MSTest, si vede che vengono superati. I dettagli di esecuzione vengono inoltre visualizzati nella finestra di dialogo Esplora test.

Figura 2

Figura 2

È possibile eseguire code coverage per i test da Esplora test. A tale scopo, fare clic con il pulsante destro del mouse sul test e scegliere Analizza code coverage per i test selezionati. I risultati vengono visualizzati nella finestra Risultati code coverage. Per attivare la raccolta dei dati per code coverage, configurare il file Local.testsettings in Elementi di soluzione. Aprendo questo file, si avvia l'editor Impostazioni test.

Se si dispone di una soluzione che non include il file Local.testsettings, aggiungerlo alla soluzione mediante la seguente procedura:

  1. Fare clic sul pulsante Aggiungi nuovo elemento.

  2. Selezionare Test, quindi fare clic su Impostazioni test.

    Figura 3
    Figura 3

  3. Fare clic sulla scheda Dati e diagnostica e selezionare la casella di controllo a destra della riga Code coverage.

  4. Fare quindi clic sul pulsante ConfiguraFigura A.

  5. Nella finestra con idettagli di code coverage selezionare tutti gli assembly da testare e fare clic su OK.

    Figura 4
    Figura 4

  6. Per chiudere l'editor Impostazioni test, fare clic su Applica e Chiudi.

  7. Eseguire di nuovo i test e fare clic sul pulsante Code coverage. I risultati di code coverage dovrebbero essere simili a quelli illustrati nella figura 5.

    Figura 5
    Figura 5

  8. Fare clic sull'icona Mostra colorazione code coverage, quindi scorrere verso il basso fino a un metodo nella griglia.

  9. Fare doppio clic sul metodo. Il relativo codice sorgente avrà un colore che indica le aree testate. Il verde indica che il codice è stato testato, mentre il grigio indica che il codice è stato testato parzialmente o non è stato testato affatto.

    Figura 6
    Figura 6

È importante creare manualmente gli unit test, ma Code Digger consente anche di aggiornarli in modo intelligente, in quanto prova valori di parametri che potrebbero non essere stati considerati. Dopo aver installato Code Digger, è possibile esplorare un metodo facendo clic su di esso con il pulsante destro del mouse nell'editor del codice e scegliendo la voce digenerazione della tabella degli input/output.

Figura 7

Figura 7

Poco dopo Code Digger completerà l'elaborazione e i risultati si stabilizzeranno. Ad esempio, la figura 8 mostra i risultati dell'esecuzione di Code Digger sul metodo TryProcessOrder del ruolo di lavoro. Notare che Code Digger è riuscito a creare un test che ha dato come risultato un'eccezione. Code Digger inoltre mostra gli input che ha creato per generare l'eccezione, ossia un valore null per il parametro relativo alla coda, e questo aspetto è ancora più importante.

Figura 8

Figura 8

Mostra: