Mocking view mapped DbSets with XUnit in EF Core
Image from Pexels by cottonbro

Mocking view mapped DbSets with XUnit in EF Core

Writing unit tests for view bounded entities in EF Core

Using in-memory DbContext is a great way to mock your actual DbContext in unit tests. You basically initialize the instance of your DbContext and seed some random data before you assert the result of your repository method.

Now the problem occurs if you are mapping database view to an entity. Views do not necessary need to have primary keys and in fact when you write configuration for an entity which comes from the view, you would often use HasNoKey that indicates you are mapping the view.

I created a view on top of database sample from article Accessing multiple databases from the same DbContext in EF Core to pair planets. each planed from the data is mapped to a all other planets to produce pairs

USE [Database1]
GO

CREATE OR ALTER VIEW [dbo].[vwPlanetPairs]
AS
	SELECT
	p1.Id, 
	p1.Name,
	CONCAT(p1.Name, ' - ', p2.Name) as Pair
	FROM Database2.dbo.Planets p1
	join Database2.dbo.Planets p2 on p1.Id!=p2.Id
GO

    

Now when we run a query to select all from this view, we get the following resut

 Sample

In order to access this view via the DbContext, we need to configure the domain value that will represent the record from the view query result.

    public class PlanetPairsConfiguration : IEntityTypeConfiguration<PlanetPair>
    {
        public void Configure(EntityTypeBuilder<PlanetPair> builder)
        {
            builder.ToView("Planets", "Database2.dbo");
            builder.HasNoKey();
        }
    }
    

Once the configuration is in place, we just need to reference it in our DbContext and we can easily do this with ApplyConfigurationsFromAssembly extension method of ModelBuilder class

    public class Database1Context : DbContext
    {
        public Database1Context(
            DbContextOptions<Database1Context> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
        }

        public DbSet<Planet> Planets { get; set; }
        public DbSet<PlanetPair> PlanetPairs { get; set; }
    }
    

This way we do not need to add a line every time we introduce new entity configuration since it will be picked up from assembly instance.

To access the data, we just need DbSet property as with any other entity. 

Unable to save entity without primary key

The problem with unit testing view entities is that you cannot seed them to in-memory DbContext which you would typically use in your unit test. One thing you can do it to have one ore more columns declared as a key when running in unit test, while you keep HasNoKey in the actual application runtime.

Let's write a simple unit test that will try to seed the data first with the DbContext to in-memory database.

    public class DataTests
    {
        [Fact]
        public async Task PlanetPairs_Should_Return_Pairs()
        {
            //Arrange
            var options = new DbContextOptionsBuilder<Database1Context>()
                 .UseInMemoryDatabase("InMemoryDb")
                 .Options;
            var _dbContext = new Database1Context(options);

            await _dbContext.PlanetPairs.AddRangeAsync(
                new PlanetPair() { Id = 1, Name = "Earth", Pair = "Earth - Mars" },
                new PlanetPair() { Id = 2, Name = "Saturn", Pair = "Saturn - Neptune" },
                new PlanetPair() { Id = 3, Name = "Saturn", Pair = "Saturn - Venus" }
                );
            await _dbContext.SaveChangesAsync();

            //Act
            var result = await _dbContext.PlanetPairs.Select(p => p).ToArrayAsync();

            //Assert
            Assert.NotNull(result);
            Assert.NotEmpty(result);
        }
    }
    

If you try to run this unit test, you will get the following exception in in test details summary pane

Xuint Dbcontext View

In simple words you need to have a primary key column in order to store the data via DbContext and if you check the data retrieved by the view, you can see that we have duplicated for the ID column. That is why we need an artificial key but only when we are running this code as part of unit test.

IHostingEnvironment to the rescue

To overcome this problem in unit tests is to have key configured dynamically. This means if we are running the code as part of unit test we will have the key in place in order to seed the mock data. Otherwise, we do not have the key when we are working with actual data. 

One way is to use environment variable to identity whether we are running inside unit test or not. In order to do so, you need to inject the IHostingEnvironment to entity configuration constructor. Currently we use extension method that scans the assembly and adds all configurations using parameterless constructor.

    public class PlanetPairsConfiguration : IEntityTypeConfiguration<PlanetPair>
    {
        readonly IHostingEnvironment _environment;
        public PlanetPairsConfiguration(IHostingEnvironment environment)
        {
            _environment = environment;
        }

        public void Configure(EntityTypeBuilder<PlanetPair> builder)
        {
            builder.ToView("Planets", "Database2.dbo");

            if (_environment.EnvironmentName.Equals("XUnit", System.StringComparison.InvariantCulture))
                builder.HasKey(k => new { k.Id, k.Name, k.Pair });
            else
                builder.HasNoKey();
        }
    }
    

Extension method ApplyConfigurationsFromAssembly will not work anymore in our case, so we will have to write out own logic to pick up all configurations and use IHostingEnvironment from the constructor for those that need it. You wont need to do this for other entity configurations which are configuring entities which have primary key. This approach maintains backward compatibility with existing entity configuration which can save you some significant amount of time since this may be needed in the middle of the project when you already have a lot of entity configurations already in place.

We first need to modify our DbContext, especially the way configurations are loaded. Remember, we still have existing configurations which are just fine with their parameterless constructors and we do not want to change that.

We need to find the ones that have IHostingEnvironment as a parameter and pass the current environment name to them while we just simple crate an instance with Activator for the ones that do not have this parameter inside the constructor.

    public class Database1Context : DbContext
    {
        #region Dynamic configuration load
        static readonly IEnumerable<(Type ConfigType, Type EntityType)> _configTypeInfos = typeof(Database1Context).Assembly.GetTypes()
                       .Where(c => c.GetInterfaces()
                           .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>)))
                                   .Select(c => (
                                       ConfigType: c,
                                       EntityType:
                                       c.GetInterfaces()
                                           .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>))
                                           ?.GetGenericArguments().FirstOrDefault()
                                   ))
                                   .Where(t => t.EntityType != null).ToArray();

        static readonly MethodInfo _applyGenericMethod = typeof(ModelBuilder).GetMethod("ApplyConfiguration", BindingFlags.Instance | BindingFlags.Public);
        #endregion

        readonly IHostingEnvironment _environment;

        public Database1Context(
            DbContextOptions<Database1Context> options,
            IHostingEnvironment environment
            ) : base(options)
        {
            _environment = environment;
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            //modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);

            foreach (var configTypeInfo in _configTypeInfos)
            {
                var hasCtorWithEnvironment = configTypeInfo.ConfigType.GetConstructor(new[] { typeof(IHostingEnvironment) }) != null;
                object config;

                if (hasCtorWithEnvironment)
                    config = Activator.CreateInstance(configTypeInfo.ConfigType, _environment);
                else
                    config = Activator.CreateInstance(configTypeInfo.ConfigType);

                var applyConcreteMethod = _applyGenericMethod.MakeGenericMethod(configTypeInfo.EntityType);
                applyConcreteMethod.Invoke(modelBuilder, new object[] { config });
            }
        }

        public DbSet<Planet> Planets { get; set; }
        public DbSet<PlanetPair> PlanetPairs { get; set; }
    }
    

It looks a bit complex especially inside the logic for the static field, it it is basically reflection picking up all the configurations from the assembly and storing it as a collection of tuples.

Note

You may ask why this is static?Well, because this is all related to a type and not actual instance, does not make much sense to tie it to the DbContext instance. Technically you need this to be executed only once when your application starts and every DbContext instance will use the same thing initially loaded in the AppDomain.Another reason is optimisation. Reflection is heavy to execute, it is simply not designed for performance, but rather for flexibility. That is one more reason you do not want to loop through you types every time you make a request for example in ASP.NET Core application.

Now to adjust our unit test method to mock IHostingEnvironment and pass it to the DbContext instance which will then pass it to every configuration that requires it.

    public class DataTests
    {
        [Fact]
        public async Task PlanetPairs_Should_Return_Pairs()
        {
            //Arrange
            var environment = new Mock<IHostingEnvironment>();
            environment.Setup(e => e.EnvironmentName).Returns("XUnit");

            var options = new DbContextOptionsBuilder<Database1Context>()
                 .UseInMemoryDatabase("InMemoryDb")
                 .Options;
            var _dbContext = new Database1Context(options, environment.Object);

            await _dbContext.PlanetPairs.AddRangeAsync(
                new PlanetPair() { Id = 1, Name = "Earth", Pair = "Earth - Mars" },
                new PlanetPair() { Id = 2, Name = "Saturn", Pair = "Saturn - Neptune" },
                new PlanetPair() { Id = 3, Name = "Saturn", Pair = "Saturn - Venus" }
                );
            await _dbContext.SaveChangesAsync();

            //Act
            var result = await _dbContext.PlanetPairs.Select(p => p).ToArrayAsync();

            //Assert
            Assert.NotNull(result);
            Assert.NotEmpty(result);
        }
    }
    

When ran from the test explorer in Visual Studio, you will have this unit test method passing

Xuint Dbcontext View Pass

 

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article