Integration Testing GraphQL Endpoints with Alba

I’m helping a JasperFx Software client get a new system off the ground that’s using both Hot Chocolate for GraphQL and Marten for event sourcing and general persistence. That’s led to a couple blog posts so far:

Today though, I want to talk about some early ideas for automating integration testing of GraphQL endpoints. Before I show my intended approach, here’s a video from ChiliCream (the company behind Hot Chocolate) showing their recommendations for testing:

Now, to be honest, I don’t agree with their recommended approach. I played a lot of sports growing up in a small town, and one of my coach’s favorite sayings actually applies here:

If you want to be good, practice like you play

every basketball coach I ever played for

That saying really just meant to try to do things well in practice so that it would carry right through into the real games. In the case of integration testing, I want to be testing against the “real” application configuration including the full ASP.Net Core middleware stack and the exact Marten and Hot Chocolate configuration for the application instead of against a separately constructed IoC and Hot Chocolate configuration. In this particular case, the application is using multi-tenancy through a separate database per tenant strategy with the tenant selection at runtime being ultimately dependent upon expected claims on the ClaimsPrincipal for the request.

All that being said, I’m unsurprisingly opting to use the Alba library within xUnit specifications to test through the entire application stack with just a few overrides of the application. My usual approach with xUnit.Net and Alba is to create a shared context that manages the lifecycle of the bootstrapped application in memory like so:

public class AppFixture : IAsyncLifetime
{
    public IAlbaHost Host { get; private set; }

    public async Task InitializeAsync()
    {
        // This is bootstrapping the actual application using
        // its implied Program.Main() set up
        Host = await AlbaHost.For<Program>(x => { });
    }

Right off the bat, we’re bootstrapping our application with its own Program.Main() entry point, but Alba is using WebApplicationFactory behind the scenes and swapping in the in memory TestServer in place of Kestrel. It’s also possible to make some service or configuration overrides of the application at this time.

The xUnit.Net and Marten mechanics I’m proposing for this client are very similar to what I wrote in Automating Integration Tests using the “Critter Stack” earlier this year.

Moving on to the GraphQL mechanics, what I’ve come up with so far is to put a GraphQL query and/or mutation in a flat file within the test project. I hate not having the test inputs in the same code file as the test, but I’m trying to offset that by spitting out the GraphQL query text into the test output to make it a little easier to troubleshoot failing tests. The Alba mechanics — so far — look like this (simplified a bit from the real code):

    public Task<IScenarioResult> PostGraphqlQueryFile(string filename)
    {
        // This ugly code is just loading up the GraphQL query from
        // a named file
        var path = AppContext
            .BaseDirectory
            .ParentDirectory()
            .ParentDirectory()
            .ParentDirectory()
            .AppendPath("GraphQL")
            .AppendPath(filename);

        var queryText = File.ReadAllText(path);

        // Building up the right JSON to POST to the /graphql
        // endpoint
        var dictionary = new Dictionary<string, string>();
        dictionary["query"] = queryText;

        var json = JsonConvert.SerializeObject(dictionary);

        // Write the GraphQL query being used to the test output
        // just as information for troubleshooting
        this.output.WriteLine(queryText);

        // Using Alba to run a GraphQL request end to end
        // in memory. This would throw an exception if the 
        // HTTP status code is not 200
        return Host.Scenario(x =>
        {
            // I'm omitting some code here that we're using to mimic
            // the tenant detection in the real code

            x.Post.Url("/graphql").ContentType("application/json");

            // Dirty hackery.
            x.ConfigureHttpContext(c =>
            {
                var stream = c.Request.Body;
                
                // This encoding turned out to be necessary
                // Thank you Stackoverflow!
                stream.WriteAsync(Encoding.UTF8.GetBytes(json));
                stream.Position = 0;
            });
        });
    }

That’s the basics of running the GraphQL request through, but part of the value of Alba in testing more traditional “JSON over HTTP” endpoints is being able to easily read the HTTP outputs with Alba’s built in helpers that use the application’s JSON serialization setup. I was missing that initially with the GraphQL usage, so I added this extra helper for testing a single GraphQL query or mutation at a time where there is a return body from the mutation:

    public async Task<T> PostGraphqlQueryFile<T>(string filename)
    {
        // Delegating to the previous method
        var result = await PostGraphqlQueryFile(filename);

        // Get the raw HTTP response
        var text = await result.ReadAsTextAsync();

        // I'm using Newtonsoft.Json to get into the raw JSON
        // a little bit
        var json = (JObject)JsonConvert.DeserializeObject(text);

        // Make the test fail if the GraphQL response had any errors
        json.ContainsKey("errors").ShouldBeFalse($"GraphQL response had errors:\n{text}");

        // Find the *actual* response within the larger GraphQL response
        // wrapper structure
        var data = json["data"].First().First().First().First();

        // This would vary a bit in your application
        var serializer = JsonSerializer.Create(new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });

        // Deserialize the raw JSON into the response type for
        // easier access in tests because "strong typing for the win!"
        return serializer.Deserialize<T>(new JTokenReader(data));
    }

And after all that, that leads to integration tests in test fixture classes subclassing our IntegrationContext base type like this:

public class SomeTestFixture : IntegrationContext
{
    public SomeTestFixture(ITestOutputHelper output, AppFixture fixture) : base(output, fixture)
    {
    }

    [Fact]
    public async Task perform_mutation()
    {
        var response = await this.PostGraphqlQueryFile<SomeResponseType>("someGraphQLMutation.txt");

        // Use the strong typed response object in the
        // "assert" part of your test
    }
}

Summary

We’ll see how it goes, but already this harness helped me out to have some repeatable steps to tweak transaction management and multi-tenancy without breaking the actual code. With the custom harness around it, I think we’ve made the GraphQL endpoint testing be somewhat declarative.

Leave a comment