Search code examples
.netasp.net-coretestingnunitintegration-testing

Avoid shared memory between ASP.NET Core integration tests


I am writing integration tests for an ASP.NET Core application using NUnit. I observed a weird behaviour in my tests, were it seems as if all tests would share the same static properties inside my SUT.

I am using WebApplicationFactory to bootstrap my app and interact with it. My assumption was that I was using a 'fresh' instance of my app per WebApplicationFactory, and I was using one factory per test...

I created a minimal reproducible example, where I just have a static int field that is incremented at startup. An endpoint returns the value of the field.

My test gets repeated multiple times and is expected to always return 1. But as you can expect, it fails during the second run.

Program.cs:

internal class Program
{
    private static int a = 0;

    private static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        var app = builder.Build();

        a++;
        app.MapGet("/test", () => a);
        app.Run();
    }
}

Unit test:

using Microsoft.AspNetCore.Mvc.Testing;

namespace Tests
{
    public class Tests
    {
        [Test]
        [Repeat(3)]
        public async Task Test1()
        {
            var factory = new WebApplicationFactory<Program>();
            var client = factory.CreateClient();
            var res = await client.GetAsync("/test");
            var number = int.Parse(await res.Content.ReadAsStringAsync());
            Assert.That(number, Is.EqualTo(1));
        }
    }
}

I am sure it has something to do with how WebApplicationFactory starts up the program, but it is still very unexpected and there was no mention about it when I read the docs.

Maybe it has something to do with me using NUnit instead of xUnit as in the docs? But then how does the behaviour of NUnit affect this?

I also tried using NUnits [NonParallelizable] to make sure the tests are run sequentially (this will be required anyway), and in my non-sample project I was using one class per test.

I spotted this behaviour, because I was registering ClassMaps with BsonClassMap at startup and I got an error about duplicate keys in my tests.

I am mostly looking for an answer on how to run my tests in a way, that they do not share state. I am also curious why this behaviour is even happening and if it is intended or not.


Solution

  • WebApplicationFactory for minimal hosting basically runs Assembly.EntryPoint for every instance factory instance (which is your Main method) hence you will have your a++; invoked for every created and "started" instance of the factory (it includes calls like CreateClient, or Services or Server) and since a is a static field it will be shared through the whole app lifetime (basically by definition).

    There are at least the following options:

    1. Move from using statics to singleton services. I.e. encapsulate a in some service, register it as singleton and resolve where appropriate. Arguably it is a better approach in general case.

    2. Use a single instance of WebApplicationFactory for all tests (so the setup is performed only a single time). For example using OneTimeSetUp combined with root fixture. For example adding something like the following to the root namespace of the test project (see the SetUpFixture docs):

      [SetUpFixture]
      public class GlobalFixture
      {
          public static WebApplicationFactory<Program> ApplicationFactory;
      
          [OneTimeSetUp]
          public void Setup() => ApplicationFactory = new WebApplicationFactory<Program>();
      
          [OneTimeTearDown] 
          public void TearDown() => ApplicationFactory.Dispose();
      }
      

      And using GlobalFixture.ApplicationFactory where needed.

    3. In some cases you can workaround by moving managing a to static constructor (add it to your Program class).

    4. Another hacky approach would be to modify the entrypoint (the Program.Main) with some conditional logic (possibly via introducing another static field), something like if(a == 0) a++;.

    My assumption was that I was using a 'fresh' instance of my app per WebApplicationFactory

    It is using a fresh instance (which I would argue actually is proved by the test failing on the second run) but you still have the same static field shared for the whole process.

    Maybe it has something to do with me using NUnit instead of xUnit as in the docs? But then how does the behaviour of NUnit affect this?

    As you can see it is not NUnit per se.