Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Unit Testing Multi-Tenant Database Providers

DZone's Guide to

Unit Testing Multi-Tenant Database Providers

My saga on supporting multiple tenants in ASP.NET Core web applications has come to point where tenants can use separate databases. It's time now to write so...

· Database Zone ·
Free Resource

RavenDB vs MongoDB: Which is Better? This White Paper compares the two leading NoSQL Document Databases on 9 features to find out which is the best solution for your next project.  

My saga on supporting multiple tenants in ASP.NET Core web applications has come to the point where tenants can use separate databases. It's time now to write some tests for data context to make sure it behaves correctly in unexpected situations. This post covers unit tests for data context and multi-tenancy.

My Previous Work on Multi-Tenancy

To get a better understanding of my previous work on multi-tenancy in ASP.NET Core and Entity Framework Core 2.0, please go through the following posts, as the current one builds on these:

Database Context

To make reading easier, here is a simplified version of the data context that I used in previous posts.

public class PlaylistContext : DbContext
{
    private readonly Tenant _tenant;
 
    public DbSet<Playlist> Playlists { get; set; }
    public DbSet<Song> Songs { get; set; }
 
    public PlaylistContext(DbContextOptions<PlaylistContext> options,
                            ITenantProvider tenantProvider)
        : base(options)
    {
        _tenant = tenantProvider.GetTenant();
    }
 
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_tenant.DatabaseConnectionString);
 
        base.OnConfiguring(optionsBuilder);
    }
 
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Playlist>().HasKey(e => e.Id);
        modelBuilder.Entity<Song>().HasKey(e => e.Id);
 
        base.OnModelCreating(modelBuilder);
    }
}

As every tenant has a separate database, there is no need to force tenant IDs through global query filters automatically to all domain entities.

What to Test

The database context below uses a tenant provider to detect connection strings for the database that the tenant must use. As there is a dependency to the tenant provider, it is important to write some tests to be sure that data context class behaves like expected. What could possibly go wrong?

Well, there are few things to take care of with unit tests:

  • Tenant provider is null.
  • Tenant is null.
  • Database connection string is missing.

Actually, the last thing should not happen — but never say never. It's possible that the tenant's file on the cloud is serialized by a list of custom-constructed DTOs or is modified manually, and therefore, testing for missing connection strings is justified.

Adding Checks

Before writing tests, let's add checks for invalid data to the data context class. The code below covers all three checks given above.

public class PlaylistContext : DbContext
{
    private readonly Tenant _tenant;
 
    public DbSet<Playlist> Playlists { get; set; }
    public DbSet<Song> Songs { get; set; }
 
    public PlaylistContext(DbContextOptions<PlaylistContext> options,
                            ITenantProvider tenantProvider)
        : base(options)
    {
 
        if(tenantProvider == null)
        {
            throw new ArgumentNullException(nameof(tenantProvider));
        }
 
        _tenant = tenantProvider.GetTenant();
 
        if(_tenant == null)
        {
            throw new NullReferenceException("Tenant is null");
        }
    }
 
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if(string.IsNullOrEmpty(_tenant.DatabaseConnectionString))
        {
            throw new NullReferenceException("Connection string is missing");
        }
 
        optionsBuilder.UseSqlServer(_tenant.DatabaseConnectionString);
 
        base.OnConfiguring(optionsBuilder);
    }
 
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Playlist>().HasKey(e => e.Id);
        modelBuilder.Entity<Song>().HasKey(e => e.Id);
 
        base.OnModelCreating(modelBuilder);
    }
}

Writing Tests

To test data context, some additional classes are needed in the test project. Integrations with other parts of the system should never happen in unit tests because otherwise, they are integration tests. There is a dependency to the ITenantProvider interface, and therefore, a fake tenant provider is needed for unit tests.

public class FakeTenantProvider : ITenantProvider
{
    private Tenant _tenant;
 
    public FakeTenantProvider(Tenant tenant)
    {
        _tenant = tenant;          
    }
 
    public Tenant GetTenant()
    {
        return _tenant;
    }
}

There is one more problem: ITenantProvider is used in the protected method OnConfiguring(). This method can be called by external caller only with reflection. The other option is to extend original data context and add a new instance of the  OnConfiguring() method that calls the protected version from the base class.

public class FakePlaylistContext : PlaylistContext
{
    public FakePlaylistContext(DbContextOptions<PlaylistContext> options, ITenantProvider tenantProvider) 
        : base(options, tenantProvider)
    {
    }
 
    public new void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
    }
}

Using these two classes, it is now possible to write tests for data context.

[TestClass]
public class PlaylistContextTests
{
    [TestMethod]
    [ExpectedException(typeof(ArgumentNullException))]
    public void ThrowsExpetionIfTenantProviderIsNull()
    {
        var options = new DbContextOptions<PlaylistContext>();
        new PlaylistContext(options, null);
    }
 
    [TestMethod]
    [ExpectedException(typeof(NullReferenceException))]
    public void ThrowsExceptionIfTenantIsNull()
    {
        var options = new DbContextOptions<PlaylistContext>();
        var provider = new FakeTenantProvider(null);
        new PlaylistContext(options, provider);
    }
 
    [TestMethod]
    [ExpectedException(typeof(NullReferenceException))]
    public void ThrowsExceptionIfConnectionStringIsMissing()
    {
        var options = new DbContextOptions<PlaylistContext>();
        var tenant = new Tenant { Id = 1 };
        var provider = new FakeTenantProvider(tenant);
        var builder = new DbContextOptionsBuilder();
 
        var context = new FakePlaylistContext(options, provider);
 
        context.OnConfiguring(builder);
    }
}

These tests use Microsoft testing stuff that also works on other platforms with ASP.NET Core projects. Running these tests in Visual Studio gives the following result.

All tests passed. Mission completed!

Wrapping Up

Tenant providers, when injected to main Entity Framework Core data context, introduce situations where testing is needed. And to test the context, one fake class and inherited version of the data context were needed. As there is no complex object graph to mock, the tests were short and easy. But these tests are necessary to make sure that data context handles problematic situations.

Handling missing tenants in ASP.NET Core

See also

Do you pay to use your database? What if your database paid you? Learn more with RavenDB.

Topics:
database ,asp.net core ,tutorial ,multi-tenant databases

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}