Testing

Differences in Azure DB and LocalDB in Integration Tests

We use Azure SQL databases in production and in our test environments, but for our integration tests, we use LocalDB by deploying a DACPAC to LocalDB and backing it up for reuse. The problem is that there are edge cases that are not caught due to differences between Azure SQL databases and LocalDB databases, particularly around collation.

We had a column in a table with a custom collation. With the wrong Entity Framework (EF) configuration, this didn’t work in Azure but worked in our integration tests. You do not want integration tests that work differently than production for obvious reasons. The solution was to set the LocalDB to Containment Partial. This way, collation is handled entirely by the database instance, and no fallback to default server collation will occur.

The code in our case:

public static async Task InitBackup(string systemName, IEnumerable<KeyValuePair<string, string>>? sqlCommandVariableValues = null)
{
    _systemName = systemName;
    var dacPackPath = GetDacPackPath();
    var dacMd5 = DacPacFingerprint(dacPackPath);

    _backup = $"{SystemName}_{dacMd5}.bak";

    try
    {
        SetBackupInfo();
    }
    catch (SqlException)
    {
        var backupFile = GetUserProfilePath(_backup);
        if (File.Exists(backupFile)) File.Delete(backupFile);

        var instance = new DacServices(string.Format(ConnectionStringTemplate, "master"));
        var options = new DacDeployOptions();
        sqlCommandVariableValues?.ForEach(kvp => options.SqlCommandVariableValues.Add(kvp.Key, kvp.Value));

        var tempDb = $"dacpac_{SystemName}_{Guid.NewGuid().ToString().Replace("-", "_")}";

        using (var dacPak = DacPackage.Load(dacPackPath))
            instance.Deploy(dacPak, tempDb, false, options);

        await ExecuteASync("sp_configure 'contained database authentication', 1;RECONFIGURE;");
        await ExecuteASync($"ALTER DATABASE [{tempDb}] SET CONTAINMENT = PARTIAL WITH NO_WAIT");

        var sql = $@"BACKUP DATABASE {tempDb}  
            TO DISK = '{_backup}'";
        await ExecuteASync(sql);
        SetBackupInfo();

        _ = Task.Run(() => Kill(tempDb));
    }
}

The important part is enabling contained authentication, which is required for partial containment, and then enabling partial containment for the database.

Parallel Test Execution Locally, Serial Execution on Build Agents

We encountered an issue where our tests were consuming too many resources on our build agents due to parallel execution. However, we wanted to keep the tests parallelized to reduce execution time locally. If you place a .runsettings file with parallelism enabled in the solution (.sln) folder, all solutions in that folder will automatically have parallelism enabled. You then create a buildagent.runsettings file, specified in the build pipeline, that executes tests sequentially.

The problem with this approach is that the auto settings apply to all solutions in the folder, which didn’t work well for us. We have legacy solutions that can’t run in parallel. So, my approach was to instead use an attribute in the test assemblies that supports parallelism:

[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)]

This enables parallelism locally and uses all available logical CPUs. For the build agents, I created a custom buildagent.runsettings file:

<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
  <RunConfiguration>
    <MaxCpuCount>0</MaxCpuCount>
  </RunConfiguration>
  <MSTest>
    <Parallelize>
      <Workers>1</Workers>
      <Scope>MethodLevel</Scope>
    </Parallelize>
  </MSTest>
</RunSettings>

This setup overrides parallelism when called with dotnet test –settings buildagent.runsettings on the build agents.

Getting rid of the slow Masstransit test harness

I wrote a blog about replacing the timeout based test harness with a semaphore driven one here. This made things much more robust when you want blackbox type testing, fire a number of events and wait until all have been digested and their spawned child events are digested.

This worked well and robust. But it still used the Masstransit harness for hosting. This made the InMemory bus more than twice as slow as hosting Masstransit in a service, including database I/O so probably a lot slower when only looking at bus performance.

But it’s pretty easy hosting Masstransit from a none service project like a test project. Instead of configuring with AddMassTransitTestHarness use the standard AddMasstransit extension method. Now events will not be consumed when you publish them, this is because the IHostedService haven’t been started. So that’s an easy fix. If we base the code on the IHarness from my previous blog post.

public Harness(IEnumerable<IHostedService> services)
{
    _services = services;
}

public async Task Start()
{
    var source = new CancellationTokenSource();

    foreach (var service in _services)
        await service.StartAsync(source.Token);
}

public async Task Stop()
{
    var source = new CancellationTokenSource();

    foreach (var service in _services)
        await service.StopAsync(source.Token);
}

Call Start from your test setup and stop from your test teardown. This will start the background workers for Masstransit and make sure it listens and consumes events. The service will not work unless you add logging to your IoC config.

new ServiceCollection()
    .AddLogging();

Coupled with the harness-code from previous blog post you now have a very robust and fast test harness. Full code below

(more…)

Test .NET 6 (Or any Core version) from legacy .NET Framework

I’m currently working on moving a large legacy system from .NET Framework 4.8 to .NET 6. Since this is a large system the move will take years and we need to work iteratively meaning both systems will co-exist over a few years.

This means that integration tests we already have for the legacy system now needs to execute code over two application domains running two completely different CLRs. I ended up making a little service that the legacy code can call to execute code in the new system.

First we create a new web API project with a single controller.

[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
    private static readonly Dictionary<Guid, IServiceProvider> ServiceProviders = new();

    public bool Get()
    {
        return true;
    }

    [HttpPost("SetupTest")]
    public async Task SetupTest(Guid testId, string connectionString)
    {

        var collection = new ServiceCollection();

        var provider = collection
            .AddCqs(configure => collection.AddMassTransitTestHarness(cfg =>
            {
                cfg.UsingInMemory((ctx, mem) =>
                {
                    mem.ConfigureTestHarness(ctx);
                    mem.AddOutbox(ctx);
                    mem.ConfigureEndpoints(ctx);
                });
                configure(cfg);
            }))
            .AddDbContext<PcDbContext>(b =>
            {
                b.UseSqlServer(connectionString);
            })
            .AddBusinessCore()
            .AddRepositories()
            .AddDomain()
            .AddTestHarness()
            .AddTestGuidFileRepository()
            .BuildServiceProvider();


        var harness = provider.GetRequiredService<IHarness>();
        await harness.Start();

        ServiceProviders.Add(testId, provider);
    }

    [HttpPost("TeardownTest")]
    public void TeardownTest(Guid testId)
    {
        ServiceProviders.Remove(testId);
    }


    private static readonly JsonSerializerOptions Options = new() { PropertyNameCaseInsensitive = true };

    [HttpPost("ExecuteCommand")]
    public async Task ExecuteCommand(Guid testId, string cmdType)
    {
        var provider = ServiceProviders[testId];

        var cmd = (await JsonSerializer.DeserializeAsync(HttpContext.Request.Body, Type.GetType(cmdType)!, Options))!;
        await provider.GetRequiredService<IBus>().Publish(cmd);
        await provider.GetRequiredService<IHarness>().WaitForBus();
    }
}
(more…)

A better Masstransit Test harness

At my latest customer project I choose to use Masstransit for events and Sagas. Its been a bumpy ride with outboxes and such, but now we have a pretty stable foundation to build upon. One problem have been testing. I like black box testing of our domain. Something like this.

[TestClass]
public class When_doing_a_complete_booking : BusinessTest
{
    private Booking _result;
    private DateTime _date;
  
    [TestInitialize]
    public void Context()
    {
        Guid bookingKey = Guid.Empty;
        _date = DateTime.Now.AddDays(5);
  
        _result = Given(db => /* Setup here */)
            .When(() => new SearchQuery{ Date = _date, ...})
            .And(result =>
            {
                bookingKey = result.First().BookingKey;
                return new ReserveCommand { BookingKey = bookingKey, ... };
            })
            .And(() => new ConfirmCommand
            {
                BookingKey = bookingKey, 
                ...
            })
            .Then(db => db.Set<booking>().FirstOrDefaultAsync(b => b.BookingKey == bookingKey));
    }
  
    [TestMethod]
    public void It_should_book_correctly ()
    {
        Assert.IsNotNull(_result);
        Assert.IsTrue(...);
    }
}

Masstransit harness really doesn’t support black box type testing. Chris Patterson favors a more unit testing-oriented approach, were you fire events and your Tests assert that events were consumed. You can await consumption with the built in harness, but its timeout oriented which makes it slow and unstable.

(more…)