Integration testing con ASP.NET Core e Docker Compose

di Moreno Gentili, in ASP.NET Core,

In questo script estendiamo i concetti che abbiamo visto nell'articolo su Docker e ASP.NET Core (https://www.aspitalia.com/articoli/asp.net-core/docker-aspnet-core-portare-nostri-siti-web-container.aspx) per capire come i container, una tecnologia così pratica e di semplice utilizzo, possano essere impiegati in attività quotidiane come il test delle applicazioni.

Nei precedenti script (https://www.aspitalia.com/script/1272/Unit-Testing-ASP.NET-Core.aspx e https://www.aspitalia.com/script/1273/Testare-Risposta-Action-ASP.NET-Core.aspx) abbiamo visto come lo unit testing ci aiuti ad esercitare i nostri componenti in maniera individuale (da qui la parola "unit") così da identificare i bug precocemente.
Limitarsi allo unit testing, tuttavia, risolve solo una parte del problema. Infatti, per assicurarci che il nostro codice stia effettivamente funzionando secondo la specifica, dobbiamo anche verificare che l'interazione tra i componenti sia corretta, soprattutto nei confronti di altri servizi infrastrutturali come il database e i webservice, che potrebbero essere soggetti a disservizi temporanei.

Con l'integration testing andiamo quindi a ricreare una situazione che sia quanto più simile a quella che avremmo nel server di produzione, allo scopo di esaminare il comportamento dell'applicazione nel suo scenario reale di funzionamento. Questa pratica ci permette di dare risposta a domande come le seguenti:

  • La mia applicazione funzionerà bene anche su server Linux, nonostante io l'abbia sviluppata su Windows (o viceversa)?
  • Risulterà reattiva anche con molti dati nel database e con parecchi utenti che la usano in contemporanea?
  • Sarà in grado di tollerare disconnessioni durante la comunicazione con un webservice o un database?

Grazie a Docker, possiamo predisporre un ambiente di test con il minimo lo sforzo, dato che richiede una configurazione minimale e semplici comandi. Questo è possibile grazie anche a Microsoft, che ha messo a disposizione (tra le altre) due immagini Docker per ASP.NET Core (microsoft/aspnetcore e microsoft/aspnetcore-build) e una per SQL Server (microsoft/mssql-server-linux), tutte basate su Linux.

Lo scenario che andremo a ricreare è quello di una normale applicazione ASP.NET Core Web API che usa un database SQL Server per la persistenza dei dati. Un progetto di integration test invierà richieste all'applicazione e ne verificherà le risposte. Per realizzarlo, configureremo i tre container illustrati nell'immagine.


Iniziamo installando la Docker Community Edition che possiamo ottenere gratuitamente dalla sua pagina di download: https://docs.docker.com/engine/installation/ . Per i sistemi Windows, è necessario disporre di Windows 10 Pro/Enterprise o Windows Server 2016, con virtualizzazione e Hyper-V entrambi abilitati.

Terminata l'installazione, riprendiamo lo sviluppo dell'applicazione che abbiamo creato nei precedenti script e che ora è disponibile anche su GitHub, al seguente indirizzo: https://github.com/BrightSoul/AspNetCoreIntegrationTestingWithDockerCompose .

Aggiungiamo un progetto dedicato all'integration testing e, in esso, definiamo il seguente test che usa la classe HttpClient per inviare vere e proprie richieste HTTP alla Web API.

private const string webApiBaseUrl = "http://webapi/api";
[TestMethod]
public async Task ShouldRetrieveValuesThatWereInsertedBefore()
{
  using (var client = new HttpClient())
  {
    var expectedValue = "aspitalia";
    var valuesEndpoint = $"{webApiBaseUrl}/values";
    
    //Inviamo vere e proprie richieste HTTP per inserire un valore e recuperare l'elenco dei valori esistenti
    await client.PostAsync(
    valuesEndpoint,
    new StringContent(JsonConvert.SerializeObject(expectedValue),
    Encoding.UTF8, "application/json"));
    var response = await client.GetStringAsync(valuesEndpoint);
    var values = JsonConvert.DeserializeObject(response) as JArray;
    
    Assert.IsNotNull(values);
    Assert.AreEqual(1, values.Count);
    Assert.AreEqual(expectedValue, values[0].Value<string>("value"));
  }
}

Così com'è, il test non potrà avere successo. Infatti, nel momento in cui lo eseguiamo con il comando dotnet test, l'applicazione ASP.NET Core Web API non sarà in esecuzione e il nome host "webapi" non sarà certamente raggiungibile.

Facciamo in modo che sia Docker stesso, per mezzo del suo tool Docker Compose, ad orchestrare la preparazione e l'avvio dei container necessari. Prepariamo quindi il file di configurazione docker-compose.yml nella directory principale della soluzione e definiamo i tre container test, webapi e db.

In particolare notiamo come test dipenda da webapi, che potrà essere raggiunto sulla porta 80 con il nome host "webapi". A sua volta, webapi dipende da db, che espone la porta 1433 agli altri container.

version: '3'

services:
  test:
    image: test
    build:
      context: ./tests/integration
      dockerfile: Dockerfile
    depends_on:
      - webapi
  webapi:
    image: webapi
    build:
      context: ./src
      dockerfile: Dockerfile
  expose:
      - "80"
    depends_on:
      - db
  db:
    image: microsoft/mssql-server-linux
    environment:
      ACCEPT_EULA: Y
      SA_PASSWORD: Password1
  expose:
      - "1433"

Nel progetto ASP.NET Core Web API creiamo un Dockerfile in cui inseriamo i comandi specifici per compilare e lanciare l'applicazione.

# Passo 1: ripristiniamo i pacchetti NuGet e compiliamo il sorgente
FROM microsoft/aspnetcore-build AS builder
WORKDIR /source
COPY MyWebApiApp.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish --output /app/ --configuration Release

# Passo 2: eseguiamo l'applicazione
FROM microsoft/aspnetcore
WORKDIR /app
COPY --from=builder /app .
ENV ASPNETCORE_ENVIRONMENT Production
ENTRYPOINT ["dotnet", "MyWebApiApp.dll"]

Mentre, nel progetto di integration testing, creiamo il seguente Dockerfile.

FROM microsoft/aspnetcore-build
WORKDIR /app
COPY . .
RUN dotnet restore
ENTRYPOINT ["dotnet", "test"]

La configurazione è completa: non resta che lanciare il comando docker-compose per preparare le immagini ed avviare i relativi container.

docker-compose -f docker-compose.yml build &&^
docker-compose -f docker-compose.yml up --force-recreate --abort-on-container-exit

Con il flag --abort-on-container-exit ci assicuriamo che i container vengano tutti terminati alla conclusione dei test. Inoltre, è importante che i container vengano ricreati ad ogni esecuzione grazie a --force-recreate, in modo che i test possano produrre risultati deterministici.

Al primo avvio, il comando docker-compose richiederà qualche minuto per scaricare le immagini dal Docker Hub (circa 2GB). Subito dopo vedremo apparire le voci di log dai 3 container, tra cui la conferma che i test sono stati eseguiti correttamente.


In pochissimo tempo siamo riusciti a testare la nostra applicazione su sistema Linux senza dover necessariamente installare il sistema operativo in una macchina virtuale. Inoltre, grazie a Docker, abbiamo potuto configurare un ambiente di testing estremamente portabile, eseguibile anche dai nostri collaboratori o da un eventuale server di Continuous Integration, che potrà lanciare i test periodicamente o dopo ogni commit.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi