blexin

Sviluppo
Consulenza e
Formazione IT


Blog

Evolvi la tua azienda

GraphQL: vediamo come testare query e mutation in ASP.NET Core con Hot Chocolate

Testiamo la nostra API con GraphQL e Hot Chocolate

Mercoledì 1 Luglio 2020

Nel mio precedente articolo, ho mostrato cos'è GraphQL e come creare una API con HotChocolate. Partendo proprio dal codice che vi condivisi sul mio repo https://github.com/AARNOLD87/GraphQLWithHotChocolate vorrei mostrarvi come poter testare il progetto.

Non si tratterà di un test di unità, perché non andremo a testare il service o il gateway, ma andremo a realizzare dei test di integrazione. Tali test serviranno per capire se l’intero flusso, dall’input alla risposta, sta funzionando correttamente.

Ho utilizzato questi test anche per essere sicuro che la mia API fornisse tutti i dati richiesti. È vero che, con GraphQL, è l’utilizzatore a specificare quali dati vuole ricevere, ma è altrettanto vero che, se quei dati non sono stati previsti, non potranno mai essere forniti come risposta. Infatti, l’utilizzatore può richiedere solo un sottoinsieme dei dati esposti dalla API. Dopo aver clonato il repo, aggiungiamo un nuovo progetto Nunit Test Project (.NET Core) alla solution:

Tale progetto ha già i pacchetti NuGet di Nunit, Nunit3TestAdapter e Microsoft.NET.Test.Sdk che ci occorrono per poter creare ed eseguire test. Aggiungiamo anche Microsoft.AspNetCore.Mvc.Core. Assicuriamoci di aver installato tutto correttamente, eseguendo il test creato dal template.

Ora che è tutto configurato possiamo creare classi che testano le nostre query GraphQL. Iniziamo con creare la classe che testa la query sui libri. Creiamo la classe BookQueryTest e il metodo ShouldReturnBooks nella quale vogliamo testare che la seguente query venga gestita in maniera corretta da GraphQL.

query {
    books {
        nodes {
            id
            title
            price
        }
    }
}

HotChocolate mette a disposizione un QueryExecutor, che a fronte di una query request restituisce il risultato dell’esecuzione. Quindi, se nel nostro test facciamo il setup del IoC container, possiamo poi chiedere al ServiceProvider di restituire un’istanza di IQueryExecutor. Una possibile soluzione potrebbe essere quella di creare un IoC container ad hoc per i test in cui andare a registrare le implementazioni mock per le rispettive interfacce, ma ho preferito utilizzare lo stesso motore IoC dell’applicazione.

IServiceCollection services = new ServiceCollection();
var startup = new Startup();
startup.ConfigureServices(services);
var provider = services.BuildServiceProvider();

A questo punto possiamo farci restituire dal provider un’istanza di IQueryExecutor

var executor = provider.GetService<IQueryExecutor>();

Prepariamo la richiesta:

IReadOnlyQueryRequest request =
    QueryRequestBuilder.New()
        .SetQuery(BooksQuery)
        .SetServices(provider)
        .Create();

In questo caso, BooksQuery è la query che vogliamo testare. La fase di arrange è terminata, possiamo passare la nostra request all’executor per la fase di act.

IExecutionResult result = await executor.ExecuteAsync(request);

Per poter fare gli opportuni assert, dobbiamo rendere result più leggibile. HotChocolate mette a disposizione un extensions method che trasforma result in un JSON.

var resultJson = result.ToJson();

La variabile resultJson è senza dubbio leggibile ma ancora risulta difficile riuscire a fare uno o più assert. Possiamo servirci di ApprovalTests: una libreria che salva un file nel nostro progetto, che terrà poi come campione per confrontarlo con i successivi risultati del test. Aggiungiamola quindi al progetto di test mediante NuGet.

Il nostro assert sarà:

Approvals.Verify(resultJson);

Aggiungiamo la seguente annotazione alla classe, come da documentazione:

[UseReporter(typeof(DiffReporter))]

Questo permetterà alla libreria di individuare il tipo di confronto che dovrà fare quando invochiamo il metodo Verify. Siamo pronti a lanciare il nostro test, e se tutto va a buon fine, avremo un test rosso:

Guardando tra i file della soluzione possiamo, notare l’aggiunta di un nuovo file

BookQueryTest.ShouldReturnBooks.received.txt:

All’interno di questo file troveremo il risultato dell’esecuzione della query:

{
  "data": {
    "books": {
      "nodes": [
        {
          "id": "1",
          "title": "First Book",
          "price": 10.0
        },
        {
          "id": "2",
          "title": "Second Book",
          "price": 11.0
        },
        {
          "id": "3",
          "title": "Third Book",
          "price": 12.0
        },
        {
          "id": "4",
          "title": "Fourth Book",
          "price": 15.0
        }
      ]
    }
  }
}

Perfetto direi, è proprio quello che dovevamo aspettarci. Rinominiamo il file sostituendo received con approved:

Lanciamo di nuovo il test:

Test verde, ottimo. Adesso dobbiamo solo testare le mutation, creando un test che verifica la creazione di un libro. Aggiungiamo la classe CreateBookMutationTest al progetto, e al suo interno creiamo il metodo ShouldCreateBook. I passi da fare sono esattamente quelli del test precedente, cambieremo solo la query che passeremo al QueryRequestBuild:

mutation {
    createBook(inputBook: {authorId: 4, price:50, title:""Test book""}) {
        price
        title
         authorId
    }
}

Avviando il test, anche in questo caso avremo un fallimento. Chiedendo di specificare il file di riferimento, controlliamo che CreateBookMutationTest.ShouldCreateBook.received.txt contenga i valori attesi:

{
    "data": {
        "createBook": {
            "price": 50.0,
             "title": "Test book",
             "authorId": 4
        }
    }
}

Rinominiamo il file come fatto in precedenza, e rilanciamo il test: se tutto va come previsto sarà verde!
È possibile passare dei parametri alla query, così da non avere i valori di input preimpostati.
Proviamo ad applicare questo metodo alla query CreateBook: lo scopo è quello di non avere l’id autore, il prezzo e il titolo già nella query, ma vogliamo che sia passato ogni volta nella fase di creazione richiesta.
Questo ovviamente ha un vantaggio fondamentale, ovvero quello di poter testare la query con diversi input modificando solo i valori passati.
Ecco come cambia la query CreateBook:

mutation($title: String, $price: Decimal!, $authorId: Int!) {
    createBook(inputBook: {authorId: $authorId, price:$price, title:$title}) {
        price
        title
        authorId
   }
}

I parametri di input vanno dichiarati subito dopo la parola chiave mutation e successivamente dove vogliamo che vengano utilizzati. Oltre al nome del parametro va specificato il tipo, mentre il punto esclamativo sta ad indicare che è un parametro obbligatorio.
Ora non resta altro che andare a specificarli nel QueryRequestBuilder mediante il metodo AddVariableValue

IReadOnlyQueryRequest request =
    QueryRequestBuilder.New()
        .SetQuery(CreateBookQuery)
        .SetServices(provider)
        .AddVariableValue("title", "Test book")
        .AddVariableValue("authorId", 4)
        .AddVariableValue("price", 50.0)
        .Create();

Abbiamo visto i due possibili casi. Le altre query e mutation esposte dalla API vanno testate esattamente allo stesso modo. In un progetto più complesso, la sezione di arrange potrebbe essere più complessa non limitandosi alla sola creazione del IoC container ma anche alla creazione di dati ad hoc per poter verificare la correttezza delle query di recupero informazioni. Act e Assert, invece, potranno continuare ad essere così come sono.

Spero di avervi incuriosito.

Al prossimo articolo!

Autore

ISCRIVITI ALLA NEWSLETTER

Servizi

Evolvi la tua azienda