facebook

Blog

Stay updated

GraphQL: let’s see how we can test queries and mutation in ASP.NET Core with Hot Chocolate
Testing our API with GraphQL and Hot Chocolate
Wednesday, July 01, 2020

In my previous article. i showed what GraphQL is and how we can create an API with HotChocolate. Starting from the code on my repo https://github.com/AARNOLD87/GraphQLWithHotChocolate let’s see how we can test the project.

This is not a unit test, because we are not going to test the service or the gateway, but we are going to carry out the integration tests. These tests will help you understand if the whole flow, from input to response, is working properly.

I used these test to make also sure my API provided all the required data. It is true that. using GraphQL, the user specifies which data he wants to receive, but it is also true that, if those data have not been predicted, they can never be provided as an answer. In fact, the user can only request a subset of the data displayed from the API. After cloning the repo, we add a new project Nunit Test Project (.Net Core) to the solution :

That project already has the NuGet package of Nunit, Nunit3TestAdapter and Microsoft.NET.Test.Sdk, that we need in order to create and perform tests.

We also add Microsoft.AspNetCore.Mvc.Core. Let’s make sure we have installed everything correctly by running the test created by the template.

Now that we have set it up, we can create classes that test our GraphQL queries.

Let’s create a class that tests the query on the books. Let’s create BookQueryTest class with the ShouldReturnBooks method, in which we want to test that GraphQL correctly manages the following query.

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

HotChocolate provides a QueryExecutor, which returns the result of the execution in view of query request. So if in our test we do the setup of the IoC container, we can then ask the ServiceProvider to return an instance of IQueryExecuter. A possible solution could be to create a specific IoC container for the testing in which to record mock implementations for the respective interfaces, but i preferred to use the same IoC engine as the application.

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

At this point we can ask the provider to return an instance of IQueryExecutor

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

Let’s build the request:

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

In this case, BooksQuery is the query that we want to test. The arrange phase is over, we can pass our request to the executor for act phase.

IExecutionResult result = await executor.ExecuteAsync(request);

In order to make the appropriate asserts, we must make the result more readable.

HotChocolate provides an extension method that turns result in a JSON.

var resultJson = result.ToJson();

ResultJson is readable but it is still difficult to make one or more asserts.

We can use ApprovalTests: it is a library that saves a file in our project and then will hold it as a sample to compare with the subsequent test results.

Let’s add it to the test project through NuGet.

Our assert will be:

Approvals.Verify(resultJson);

Let’s add the following annotation to the class, as described in the documentation:

[UseReporter(typeof(DiffReporter))]

This annotation will allow the library to pinpoint the type of comparison itl needs to make when we invoke the Verify method. We are ready to launch our test and, if all end well, we will have a red test:

Looking among the files of the solution, we can see that new file has been added

BookQueryTest.ShouldReturnBooks.received.txt:

Within this file we will find the result of the execution of the 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
        }
      ]
    }
  }
}

Perfect, it is as expected. We rename the file replacing received with approved:

Running the test again:

It’s green, excellent. Now we must test the mutations, creating a test that verifies the creation of a book. Let’s add the class CreateBookMutationTest to the project, and create the method ShouldCreateBookinside it. The steps to take are exactly those of the previous test, we will change only the query that we will pass to the QueryRequestBuild:

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

If we start the test, it will fail again. Asking to specify the reference file, we check that CreateBookMutationTest.ShouldCreateBook.received.txt contains the expected values:

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

We rename the file as we did before, and we run the test again: if everything goes as planned it will be green!
You can pass parameters to the query, so that you don’t have the default input values.
We try to apply this method to the query CreateBook: the purpose is not to have the author id, the price and the title already in the query, but we want it to be passed every time in the phase of creation request.
This has a fundamental advantage, that is being able to test the query with different inputs modifying only the past values.
Here is how the CreateBook query changes

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

Input parameters must be declared immediately after the keyword mutation and then where we want them to be used. In addition to the parameter name, the type should be specified, while the exclamation mark indicates that it is a mandatory parameter.
Now all that remains is to specify them in the QueryRequestBuilder using the AddVariableValue method

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

We have seen the two possible cases. Other queries and mutations exposed by the API must be tested exactly in the same way. In a more complex project, the arrange section could be more complex, and not only creating the IoC container but also a specific data to verify the correctness of information recovery queries.

Act and Assert, however, may continue to remain the same.

I hope I intrigued you.

See you at the next article!

Written by

Adolfo Arnold