使用Moq模拟EF DbContext

时间:2014-09-21 14:24:19

标签: c# unit-testing entity-framework-5 moq dbcontext

我尝试使用模拟的DbContext为我的服务创建单元测试。我使用以下函数创建了一个接口IDbContext

public interface IDbContext : IDisposable
{
    IDbSet<T> Set<T>() where T : class;
    DbEntityEntry<T> Entry<T>(T entity) where T : class;
    int SaveChanges();
}

我的真实上下文实现了此接口IDbContextDbContext

现在我试图在上下文中模仿IDbSet<T>,因此它会返回List<User>

[TestMethod]
public void TestGetAllUsers()
{
    // Arrange
    var mock = new Mock<IDbContext>();
    mock.Setup(x => x.Set<User>())
        .Returns(new List<User>
        {
            new User { ID = 1 }
        });

    UserService userService = new UserService(mock.Object);

    // Act
    var allUsers = userService.GetAllUsers();

    // Assert
    Assert.AreEqual(1, allUsers.Count());
}

我总是在.Returns上收到此错误:

The best overloaded method match for
'Moq.Language.IReturns<AuthAPI.Repositories.IDbContext,System.Data.Entity.IDbSet<AuthAPI.Models.Entities.User>>.Returns(System.Func<System.Data.Entity.IDbSet<AuthAPI.Models.Entities.User>>)'
has some invalid arguments

6 个答案:

答案 0 :(得分:29)

我设法通过创建实现FakeDbSet<T>

IDbSet<T>类来解决它
public class FakeDbSet<T> : IDbSet<T> where T : class
{
    ObservableCollection<T> _data;
    IQueryable _query;

    public FakeDbSet()
    {
        _data = new ObservableCollection<T>();
        _query = _data.AsQueryable();
    }

    public virtual T Find(params object[] keyValues)
    {
        throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
    }

    public T Add(T item)
    {
        _data.Add(item);
        return item;
    }

    public T Remove(T item)
    {
        _data.Remove(item);
        return item;
    }

    public T Attach(T item)
    {
        _data.Add(item);
        return item;
    }

    public T Detach(T item)
    {
        _data.Remove(item);
        return item;
    }

    public T Create()
    {
        return Activator.CreateInstance<T>();
    }

    public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
    {
        return Activator.CreateInstance<TDerivedEntity>();
    }

    public ObservableCollection<T> Local
    {
        get { return _data; }
    }

    Type IQueryable.ElementType
    {
        get { return _query.ElementType; }
    }

    System.Linq.Expressions.Expression IQueryable.Expression
    {
        get { return _query.Expression; }
    }

    IQueryProvider IQueryable.Provider
    {
        get { return _query.Provider; }
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _data.GetEnumerator();
    }

    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        return _data.GetEnumerator();
    }
}

现在我的测试看起来像这样:

[TestMethod]
public void TestGetAllUsers()
{
    //Arrange
    var mock = new Mock<IDbContext>();
    mock.Setup(x => x.Set<User>())
        .Returns(new FakeDbSet<User>
        {
            new User { ID = 1 }
        });

    UserService userService = new UserService(mock.Object);

    // Act
    var allUsers = userService.GetAllUsers();

    // Assert
    Assert.AreEqual(1, allUsers.Count());
}

答案 1 :(得分:14)

感谢Gaui的好主意=)

我确实为您的解决方案添加了一些改进,并希望分享它。

  1. 我的FakeDbSet也来自DbSet以获取其他方法 像AddRange()
  2. 我将ObservableCollection<T>替换为List<T>以传递所有内容 List<>中已实施的方法,直到FakeDbSet
  3. 我的FakeDbSet:

        public class FakeDbSet<T> : DbSet<T>, IDbSet<T> where T : class {
        List<T> _data;
    
        public FakeDbSet() {
            _data = new List<T>();
        }
    
        public override T Find(params object[] keyValues) {
            throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
        }
    
        public override T Add(T item) {
            _data.Add(item);
            return item;
        }
    
        public override T Remove(T item) {
            _data.Remove(item);
            return item;
        }
    
        public override T Attach(T item) {
            return null;
        }
    
        public T Detach(T item) {
            _data.Remove(item);
            return item;
        }
    
        public override T Create() {
            return Activator.CreateInstance<T>();
        }
    
        public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T {
            return Activator.CreateInstance<TDerivedEntity>();
        }
    
        public List<T> Local {
            get { return _data; }
        }
    
        public override IEnumerable<T> AddRange(IEnumerable<T> entities) {
            _data.AddRange(entities);
            return _data;
        }
    
        public override IEnumerable<T> RemoveRange(IEnumerable<T> entities) {
            for (int i = entities.Count() - 1; i >= 0; i--) {
                T entity = entities.ElementAt(i);
                if (_data.Contains(entity)) {
                    Remove(entity);
                }
            }
    
            return this;
        }
    
        Type IQueryable.ElementType {
            get { return _data.AsQueryable().ElementType; }
        }
    
        Expression IQueryable.Expression {
            get { return _data.AsQueryable().Expression; }
        }
    
        IQueryProvider IQueryable.Provider {
            get { return _data.AsQueryable().Provider; }
        }
    
        IEnumerator IEnumerable.GetEnumerator() {
            return _data.GetEnumerator();
        }
    
        IEnumerator<T> IEnumerable<T>.GetEnumerator() {
            return _data.GetEnumerator();
        }
    }
    

    修改dbSet并模拟EF上下文对象非常容易:

        var userDbSet = new FakeDbSet<User>();
        userDbSet.Add(new User());
        userDbSet.Add(new User());
    
        var contextMock = new Mock<MySuperCoolDbContext>();
        contextMock.Setup(dbContext => dbContext.Users).Returns(userDbSet);
    

    现在可以执行Linq查询,但要知道可能不会自动创建外键引用:

        var user = contextMock.Object.Users.SingeOrDefault(userItem => userItem.Id == 42);
    

    由于上下文对象被模拟,Context.SaveChanges()将不会执行任何操作,并且您的entites的属性更改可能不会填充到您的dbSet。我通过模拟我的SetModifed()方法来填充更改来解决这个问题。

答案 2 :(得分:10)

如果有人仍然感兴趣,我遇到了同样的问题,发现这篇文章非常有帮助: Entity Framework Testing with a Mocking Framework (EF6 onwards)

它仅适用于Entity Framework 6或更新版本,但它涵盖了从简单的SaveChanges测试到使用Moq(以及一些手动类)的异步查询测试的所有内容。

答案 3 :(得分:4)

如果有人仍在寻找答案,我已经实施了small library以允许嘲笑DbContext。

第1步

安装Coderful.EntityFramework.Testing nuget包:

if (progress >= 50) { webView.stopLoading(); // do screenshot }

第2步

然后创建一个这样的类:

Install-Package Coderful.EntityFramework.Testing

第3步

现在你可以轻松地创建模拟了:

internal static class MyMoqUtilities
{
    public static MockedDbContext<MyDbContext> MockDbContext(
        IList<Contract> contracts = null,
        IList<User> users = null)
    {
        var mockContext = new Mock<MyDbContext>();

        // Create the DbSet objects.
        var dbSets = new object[]
        {
            MoqUtilities.MockDbSet(contracts, (objects, contract) => contract.ContractId == (int)objects[0] && contract.AmendmentId == (int)objects[1]),
            MoqUtilities.MockDbSet(users, (objects, user) => user.Id == (int)objects[0])
        };

        return new MockedDbContext<SourcingDbContext>(mockContext, dbSets); 
    }
}

然后使用你的模拟:

// Create test data.
var contracts = new List<Contract>
{
    new Contract("#1"),
    new Contract("#2")
};

var users = new List<User>
{
    new User("John"),
    new User("Jane")
};

// Create DbContext with the predefined test data.
var dbContext = MyMoqUtilities.MockDbContext(
    contracts: contracts,
    users: users).DbContext.Object;

完整文章:http://www.22bugs.co/post/Mocking-DbContext/

答案 4 :(得分:3)

根据this MSDN文章,我创建了自己的库,用于模拟DbContextDbSet

  • EntityFrameworkMock - GitHub
  • EntityFrameworkMockCore - GitHub

两者均可在NuGet和GitHub上使用。

我创建这些库的原因是因为我想模拟SaveChanges行为,在插入具有相同主键的模型时抛出DbUpdateException并支持多列/自动增加模型中的主键。

此外,由于DbSetMockDbContextMock都来自Mock<DbSet>Mock<DbContext,因此您可以使用Moq framework的所有功能。

在Moq旁边,还有一个NSubstitute实现。

Moq版本的用法如下:

public class User
{
    [Key, Column(Order = 0)]
    public Guid Id { get; set; }

    public string FullName { get; set; }
}

public class TestDbContext : DbContext
{
    public TestDbContext(string connectionString)
        : base(connectionString)
    {
    }

    public virtual DbSet<User> Users { get; set; }
}

[TestFixture]
public class MyTests
{
    var initialEntities = new[]
        {
            new User { Id = Guid.NewGuid(), FullName = "Eric Cartoon" },
            new User { Id = Guid.NewGuid(), FullName = "Billy Jewel" },
        };

    var dbContextMock = new DbContextMock<TestDbContext>("fake connectionstring");
    var usersDbSetMock = dbContextMock.CreateDbSetMock(x => x.Users, initialEntities);

    // Pass dbContextMock.Object to the class/method you want to test

    // Query dbContextMock.Object.Users to see if certain users were added or removed
    // or use Mock Verify functionality to verify if certain methods were called: usersDbSetMock.Verify(x => x.Add(...), Times.Once);
}

答案 5 :(得分:1)

我来晚了,但是发现这篇文章很有帮助:Testing with InMemory(MSDN文档)。

它说明了如何使用内存DB上下文(不是数据库),而无需编写代码,并且有机会实际测试DBContext实现。