Search code examples
.netentity-frameworkmoqwebapi

How to Avoid the Reference of Mock DbSet<TData> in C#?


I am mocking DbContext as well as DbSet. My problem is while retrieve data using FirstOrDefault and change the value of some property, It automatically update in DbSet before doing dbContext.Update() .

Here is my implementations,

public Mock<TContext> GetMock<TData, TContext>(List<TData> data,
        Expression<Func<TContext, DbSet<TData>>> dbSetSelectionExpression) where TData : class where TContext : DbContext
    {
        var queryableData = data.AsQueryable();
        var stubSet = new Mock<DbSet<TData>>();
        var stubContext = new Mock<TContext>();
        stubSet.As<IQueryable<TData>>().Setup(s => s.Provider).Returns(queryableData.Provider);
        stubSet.As<IQueryable<TData>>().Setup(s => s.Expression).Returns(queryableData.Expression);
        stubSet.As<IQueryable<TData>>().Setup(s => s.ElementType).Returns(queryableData.ElementType);
        stubSet.As<IQueryable<TData>>().Setup(s => s.GetEnumerator()).Returns(() => queryableData.GetEnumerator());        
        
        stubContext.Setup(dbSetSelectionExpression).Returns(stubSet.Object);
        return stubContext;
    }

var data = new List<MyEntity>
{
    new MyEntity { Id = 1, Name = "Entity 1" },
    new MyEntity { Id = 2, Name = "Entity 2" }
};

var dbContextMock=GetMock<ProductContext,MyEntity>(data,x=>x.MyEntity);

//retrieve Data
var data=dbContextMock.Object.MyEntity.FirstOrDefault(x=>x.Id==1);
//Change the value
data.Name="New Entity";
//After executing above line, it automatically update in dbContextMock.Object.MyEntity

How to Avoid the reference of Mock DbSet while retrieve data and change the data?


Solution

  • Don't mock DbContext. There's no reason and no benefit. What you try to do will end up testing your mocking code, not your controllers.

    You can easily create a DbContext backed by the in-memory provider or SQLite in in-memory mode.

    _contextOptions = new DbContextOptionsBuilder<BloggingContext>()
        .UseInMemoryDatabase("BloggingControllerTest")
        .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning))
        .Options;
    
    using var context = new BloggingContext(_contextOptions);
    context.Database.EnsureCreated();
    
    context.AddRange(
        new Blog { Name = "Blog1", Url = "http://blog1.com" },
        new Blog { Name = "Blog2", Url = "http://blog2.com" });
    
    context.SaveChanges();
    

    The in-memory provider doesn't support complex SQL, views etc. An alternative is to use SQLite in in-memory mode. The only change needed is to change the provider:

    _connection = new SqliteConnection("Filename=:memory:");
    _connection.Open();
    
    _contextOptions = new DbContextOptionsBuilder<BloggingContext>()
        .UseSqlite(_connection)
        .Options;
    

    There's no need to call DbContext.Update when you modify an object loaded using DbContext either. A DbContext is a Unit-of-Work and will detect all changes made to the objects it tracks. All you really need to load and modify an object is:

    var blog=context.Find(1);
    blog.Name="OtherName";
    await context.SaveChanges();
    

    SaveChanges will detect and persist changes to all objects. Essentially, it commits all the changes you made since you created a DbContext. There's no reason to call it after every change. To roll back all pending changes just dispose the context When you register a DbContext with AddDbContext you specify the provider with .UseSqlServer, UseSqlite or UseInMemoryDatabase. In a