Successfully Running an xUnit Suite in Parallel

TL:DR: Don’t call Task.Wait() in your xUnit tests if you want things running faster and in parallel. In other words, async turtles all the way down. This is a requested post from my buddy Jim Holmes.

In my recent OSS efforts like Marten, Jasper, and Lamar, I have tended to lean much more heavily on top down integration tests than having lots of intermediate and low level unit tests. Putting aside the wisdom of that approach aside for another time, depending so much on integration testing has made the main testing suite in Jasper run too slowly for my comfort as the project has grown.

Like many xUnit users, the second I hit issues with test suites locking up or failing unpredictably, I lazily slap on the directive to prevent parallel test execution like so:

using Xunit;

[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]

As I said, the Jasper suite got too slow for productive, quick twitch development, so I finally broke down and committed enough time to eliminate all the issues in the Jasper code and test suite that was stopping us from running the tests in parallel. It’s a huge, squashed commit, but you can see what I did here.

In a few places, I was using static members to record actions during integration tests as just a mechanically cheap way of asserting correct behavior. I had to move all of that to object instances that were scoped to the test run. Not that big of a deal.

The bigger problem by far was deadlock issues in bootstrapping a Jasper application, which was kind of a problem where there are ~200 tests that each try to bootstrap a Jasper application as part of the test. To optimize the “cold start” time of Jasper, I heavily parallelize startup activities through Task objects. The synchronous version of bootstrapping has to eventually do a couple Task.GetAwaiter().GetResult() calls (once in Jasper, once in Lamar where it uses StructureMap’s old trick of parallelizing the type scanning), and that was prone to deadlocks.

The original pattern of many integration tests looked like this:

[Fact]
public void some_name()
{
    using (var runtime = JasperRuntime.For(_ => { // some configuration}))
        // do stuff and run assertions
    });
}

After re-plumbing the bootstrapping and adding purely asynchronous bootstrapping and teardown methods to both Jasper and the underlying Lamar IoC container, I mostly moved to this pattern instead:

[Fact]
public async Task some_name()
{
    var runtime = await JasperRuntime.ForAsync(_ => {
        // some configuration
    });

    try {
        // do stuff and run assertions
    }
    finally {
        // shutdown the running app w/ 100% async API calls
        await runtime.Shutdown();
    }
}

Long story short, it sucked, but now the tests can happily run in parallel and it made a huge difference in the test suite runtime and I’ve been able to be much more productive.

Leave a comment