Moving from unit test to integration/system test

This post is not directly about Blazor but I wanted to blog about this technical change.

On my Toss project I decided to move from a Unit Test approach to an Integration test approach. In this blog post I’ll try to explain the reasons and the technique employed.

How I build my unit test

I build my unit test this way : a method (unit) has input (parameters and dependencies) and output (return, dependencies and exceptions).

A unit test will “arrange” the input :

  • the parameters are forced
  • the dependencies are setup via a mocking framework (Setup() in Moq) And “assert” the output :
  • the expected return and exceptions are checked
  • the dependencies are validated via the mocking framework (Verify() in Moq)

In theory this is perfect :

  • only production code is executed at test assert, I don’t depend on other systems.
  • if one test fails, the reason will be easy to find.
  • every piece of code will be writen following the red/green process (create test and add code until tests passes).

Problems with unit tests

This approach has drawbacks that lead me to getting rid of it :

  • Because of the mandatory mocking/faking of dependencies, you are not testing an interface but an implementation. And a method is the lower block of implementation. Every time I’ll do a small change in my implementation (for instance I change a loop on “SaveOne” for a call to “SaveAll”) I will need to change my tests.
  • Setting up everything is a lot of code, look at this file I had to create to mock the ASPNET Core Identity dependencies. If I have the simple formula “1 LoC = X bugs”, we can say that I will spend more time debugging my tests (and that’s what happened) than my actual code !
  • Because you are not testing everything altogether you can have problem at runtime : you didn’t setup DI the right way, because your class doesn’t implement the good interface, your configuration is not set …

Technical solution

The solution is system test, we could call it integration test but for some of them there might be no dependency involved. Here I want to test my system as a whole.

Use the ASPNET Core DI setup

Here is my class setting up this :

public class TestFixture
{
    public const string DataBaseName = "Tests";
    public const string UserName = "username";
    private static ServiceProvider _provider;
    //only mock we need :)
    private static Mock<IHttpContextAccessor> _httpContextAccessor;

    public static ClaimsPrincipal ClaimPrincipal { get; set; }

    static TestFixture()
    {

        var dict = new Dictionary<string, string>
        {
             { "GoogleClientId", ""},
             { "GoogleClientSecret", ""},
             { "MailJetApiKey", ""},
             { "MailJetApiSecret", ""},
             { "MailJetSender", ""},
             { "CosmosDBEndpoint", "https://localhost:8081"},
             { "CosmosDBKey", "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="},
             { "StripeSecretKey", ""},
             {"test","true" },
             {"dataBaseName",DataBaseName }
        };

        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(dict)
            .Build();
        var startup = new Startup(config);
        var services = new ServiceCollection();
        startup.ConfigureServices(services);
        _httpContextAccessor = new Mock<IHttpContextAccessor>();

        services.AddSingleton(_httpContextAccessor.Object);
        services.AddScoped(typeof(ILoggerFactory), typeof(LoggerFactory));
        services.AddScoped(typeof(ILogger<>), typeof(Logger<>));

        _provider = services.BuildServiceProvider();

    }

    public async static Task CreateTestUser()
    {
        var userManager =  _provider.GetService<UserManager<ApplicationUser>>();
        ApplicationUser user = new ApplicationUser()
        {
            UserName = UserName,
            Email = "test@yopmail.com",
            EmailConfirmed = true
        };
        await userManager.CreateAsync(user);
        ClaimPrincipal = new ClaimsPrincipal(
                  new ClaimsIdentity(new Claim[]
                     {
                                new Claim(ClaimTypes.Name, UserName)
                     },
                  "Basic"));
        (ClaimPrincipal.Identity as ClaimsIdentity).AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
        _httpContextAccessor
          .SetupGet(h => h.HttpContext)
          .Returns(() =>
          new DefaultHttpContext()
          {
              User = ClaimPrincipal

          });
    }

    public static void SetControllerContext(Controller controller)
    {
        controller.ControllerContext = new ControllerContext
        {
            HttpContext = _httpContextAccessor.Object.HttpContext
        };
    }

    public static void SetControllerContext(ControllerBase controller)
    {
        controller.ControllerContext = new ControllerContext
        {
            HttpContext  = _httpContextAccessor.Object.HttpContext
        };
    }

    public static T GetInstance<T>()
    {
        T result = _provider.GetRequiredService<T>();
        ControllerBase controllerBase = result as ControllerBase;
        if (controllerBase != null)
        {
            SetControllerContext(controllerBase);
        }
        Controller controller = result as Controller;
        if (controller != null)
        {
            SetControllerContext(controller);
        }
        return result;

    }
}
  • I have to mock the HttpContextAccessor as there is no Http Query and I need it for knowing who is the connected user
  • I pass “test” “true” to the config so I can setup my fake/mock in Configure()
  • I had to force the logger DI setup, I guess it’s set by something in ConfigureService

Choosing the tested layer

I chose to test at the mediator layer (from MediatR), my controller layer is very thin so I prefer to test my app here.

My test setup are really simple, they are basically like this :

 var mediator = TestFixture.GetInstance<IMediator>();
 
 mediator.Send(new MyCommand());
 
 var res = mediator.Send(new MyQuery());
 
 Assert.Single(res);

  • This test will uses both the command and query so with this 4 LoC I test a lot of code : DI setup, interface declaration, interface implementation …

External dependencies

I still need to mock some dependencies that I don’t manage : Stripe or MailJet or even Random. Here is how I setup the fake

// Add application services.
if (Configuration.GetValue<string>("test") == null)
{
    services.AddTransient<IEmailSender, EmailSender>();
    services.AddSingleton<IStripeClient, StripeClient>(s => new StripeClient(Configuration.GetValue<string>("StripeSecretKey")));
}
else
{
    //We had it as singleton so we can get the content later during the asset phase
    services.AddSingleton<IEmailSender, FakeEmailSender>();
    services.AddSingleton<IStripeClient, FakeStripeClient>();
}
  • The fake code are very simple (you can find it in my Toss repo) they just record the received parameters and have a static property for giving the next expected result

Internal dependencies

I call internal dependencies, dependencies that I manage entirely like CosmosDB. CosmosDB doesn’t support transaction with multiple client request like SQL Server(you have to create a server side sp for using transactions) so I have to clean up the database after each tests. Here is my base class for doing this :

public class BaseCosmosTest : IAsyncLifetime
{
    public BaseCosmosTest()
    {
    }

    public async Task InitializeAsync()
    {
    }

    public async Task DisposeAsync()
    {
        var _client = TestFixture.GetInstance<DocumentClient>();
        var collections = _client.CreateDocumentCollectionQuery(UriFactory.CreateDatabaseUri(TestFixture.DataBaseName)).ToList();
        foreach (var item in collections)
        {
            var docs = _client.CreateDocumentQuery(item.SelfLink);
            foreach (var doc in docs)
            {
                await _client.DeleteDocumentAsync(doc.SelfLink);
            }
        }
    }
}
  • I only remove the document not the collections, so my test will run faster
  • IAsyncLifetime is here for having an async Dispose method
  • I cannot clean the DB before the test as the DB will not be created until the Startup.Configure method is called
  • I need to force the test to run non parallel (don’t know if this term is correct in english but you get my point) as they all use the same DB/Collections here is the xunit.runner.json needed (you need to Copy it in output directory) :
{  
  "parallelizeTestCollections": false
}

New problems

There is of course drawbacks in this way of testing :

  • A test can fail for many reason, so in the long term debugging failing test my be more difficult than with a unit test approach.
  • You have to be able to clean all the dependencies between each test, if not they will fall in the external category. This mean writing “test only” code.
  • The test will take longer to run.
  • I don’t test as much as I want : route, controller … I could do only E2E test but they are too much pain to create, so I only have a few of them.

Reference