Search code examples
c#entity-frameworkunit-testingdbcontext

C# How to mock DbContext without passing it


I want to unit test my function GetDogName which creates a new DbContext. I already have a working Mock of my DbContext, but I can't pass it everywhere as a parameter.

I can replace the DbContext context = new() with something else in the function. But I don't have the possibility to pass the DbContext as a parameter.

I also have checked if it is possible with a factory (or something else?), but have not found a solution yet.

static class AnimalsHelper
{
    public static string GetDogName() // I can not pass parameters here
    {
        // I can change this line
        using (DbContext context = new())
        {
            return context.Dogs.First().Name;
        }
    }
}

class UnitTest
{
    [SetUp]
    public void SetUp()
    {
        ContextMock = new Mock<DbContext>();
        Context     = ContextMock.Object;

        ContextMock.Setup(context => context.Dogs).Returns("Rex");
    }

    [Test]
    public void GetDogName_DogName_ReturnsRex()
    {
        var expected = "Rex";
        var actual   = AnimalsHelper.GetDogName();
        Assert.AreEqual(expected, actual);
    }
}

Solution

  • We have solved this with a DbContextCreator class.

    An instance of a IDbContextCreator class is created in the Main and this is then kept static.
    The function to which a DbContext can not be passed as a parameter uses this static creator instance to create a new DbContext.
    In the unit test, a creator class is used, which returns a DbContextMock.

    This allows the function to be tested without accessing the database.

    Main

    internal static class Program
    {
        /// <summary>
        ///     Main application entry point
        /// </summary>
        internal static async Task Main(string[] args)
        {
            DbContextCreator.Instance = new MyDbContextCreator();
    
            // Start Application
        }
    
        /// <inheritdoc />
        private class MyDbContextCreator : IDbContextCreator
        {
            /// <inheritdoc />
            public BaseDbContext Create() => new();
        }
    }
    

    Function

    public static void TestableFunction()
    {
        using (BaseDbContext context = DbContextCreator.Create())
        {
            // DB stuff
            context.SaveChanges();
        }
    }
    

    Abstract Unit Test

    public abstract class AbstractTestWithMockData : AbstractTest
    {
        protected BaseDbContext       Context     { get; set; }
        protected Mock<BaseDbContext> ContextMock { get; set; }
    
        /// <inheritdoc />
        protected AbstractTestWithMockData()
        {
            DbContextCreator.Instance = new TestDbContextCreator(this);
        }
    
        /// <inheritdoc />
        [SetUp]
        public override void SetUp()
        {
            base.SetUp();
    
            ContextMock = new Mock<BaseDbContext>();
            Context     = ContextMock.Object;
    
            // ContextMock.Setup()
        }
    
        /// <inheritdoc />
        private class TestDbContextCreator : IDbContextCreator
        {
            private readonly AbstractTestWithMockData AbstractTest;
    
            public TestDbContextCreator(AbstractTestWithMockData abstractTest)
            {
                AbstractTest = abstractTest;
            }
    
            /// <inheritdoc />
            public BaseDbContext Create()
            {
                return AbstractTest.Context;
            }
        }
    }
    

    Creator Interface and static Instance

    /// <summary>
    ///     Interface for a project-specific implementation of a <see cref="BaseDbContext" /> creator.<br />
    ///     The creator is required in order to be able to unit test functions
    ///     where a <see cref="BaseDbContext" /> cannot be passed as a parameter.
    /// </summary>
    public interface IDbContextCreator
    {
        BaseDbContext Create();
    }
    
    /// <summary>
    ///     Static database creator that holds an instance of <see cref="IDbContextCreator" />
    ///     to be able to create an instance of <see cref="BaseDbContext" />.
    /// </summary>
    public static class DbContextCreator
    {
        /// <summary>
        ///     Static instance of <see cref="IDbContextCreator" /> for the <see cref="Create" /> function.
        /// </summary>
        [SuppressMessage("Design", "CA1044:Properties should not be write only")]
        public static IDbContextCreator? Instance { private get; set; }
    
        /// <summary>
        ///     Creates a new instance of <see cref="BaseDbContext" />.
        /// </summary>
        /// <returns>A new instance of <see cref="BaseDbContext" /></returns>
        /// <exception cref="Exception">When the <see cref="Instance" /> is not set</exception>
        public static BaseDbContext Create()
        {
            if (Instance is null)
            {
                throw new Exception("Database creator instance is not set! Covfefe!");
            }
    
            return Instance.Create();
        }
    }