使用Moq模拟EF DbContext

100

我正在尝试使用模拟的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

2
尽管这篇文章很有用,但我认为如果您包括Moq DbContext的实现,它会更加有用。感谢您提供的建议。 - LostNomad311
6个回答

49

我成功解决了这个问题,通过创建一个实现IDbSet<T>FakeDbSet<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());
}

我认为考虑@Ladislav Mrnk在这里的评论是很好的 https://dev59.com/E2w05IYBdhLWcg3w_Gyw?lq=1 和 https://dev59.com/IWw15IYBdhLWcg3wMY-L - user8128167
尝试在 .net Core 1.0 下实现这个,但是一个主要的问题是 IDbSet 已经被移除了,并且构造函数是私有的,所以我甚至无法提取自己的接口。 - Paul Gorbas
这已经不是EF6的首选方式了,因为EF6添加了新的更改到DbSet中,IDbSet没有反映(并且如上所述在Core中已被删除)。https://entityframework.codeplex.com/wikipage?title=Design%20Meeting%20Notes%20-%20May%2016%2c%202013DbSet相反更易于模拟,虽然我还不确定正确的实现方式是什么。 - IronSean
@PaulGorbas 我发现在 .NET Core 2 中使用 SqlLite 设置 DbContext 比使用 Moq 更容易,例如:https://gist.github.com/mikebridge/a1188728a28f0f53b06fed791031c89d。 - mikebridge
有关 async EF 重载的类似伪造,请参见此处 - StuartLC
你也可以返回其他的IDbSetMock,而不是FakeDbSet - realsonic

35

24

感谢Gaui的好主意=)

我对您的解决方案进行了一些改进,并想分享给大家。

  1. 我的 FakeDbSet 也继承自 DbSet,以获取其他方法,如 AddRange()
  2. 我用 List<T> 替换了 ObservableCollection<T>,以将所有已实现的方法传递到我的 FakeDbSet

我的 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()不会执行任何操作,并且实体属性的更改可能不会传递到您的dbSet中。我通过模拟我的SetModified()方法来填充更改,从而解决了这个问题。


尝试在 .net Core 1.0 下实现此功能,但一个主要的问题是 IDbSet 已被移除,并且构造函数是私有的,因此我甚至无法提取自己的接口。 - Paul Gorbas
1
这个答案解决了我在ASP.NET MVC 5中模拟EF .Add()函数的问题。虽然我知道它与Core无关,但我能够提取足够的信息来继承一个现有的类,用dbset、idbset模拟dbset,并覆盖Add方法。谢谢! - CSharpMinor
我的超级棒DbContext是一个具体类吗? - Jaydeep Shil
MySuperCoolDbContext 是你的 DbContext 的名称。 - szuuuken

11
根据这篇MSDN文章,我创建了自己的库来模拟DbContextDbSet
  • EntityFrameworkMock - GitHub
  • EntityFrameworkMockCore - GitHub

两个库都可在NuGet和GitHub上找到。

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

此外,由于DbSetMockDbContextMock都继承自Mock<DbSet>Mock<DbContext>,您可以使用Moq框架的所有功能。

除了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);
}

DbContextMock是什么? - Richard Barraclough
这是在我回答顶部提到的库中声明的类型。 - huysentruitw

7
如果还有人在寻找答案,我已经实现了一个小型库,可以允许模拟DbContext。

步骤1

安装Coderful.EntityFramework.Testing nuget包:

Install-Package Coderful.EntityFramework.Testing

步骤2

然后创建一个像这样的类:

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;

然后使用你的模拟:

// Create.
var newUser = dbContext.Users.Create();

// Add.
dbContext.Users.Add(newUser);

// Remove.
dbContext.Users.Remove(someUser);

// Query.
var john = dbContext.Users.Where(u => u.Name == "John");

// Save changes won't actually do anything, since all the data is kept in memory.
// This should be ideal for unit-testing purposes.
dbContext.SaveChanges();

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


1
猜测您没有维护与 .net core 兼容的软件包。这是我在尝试安装时收到的错误: 软件包 Coderful.EntityFramework.Testing 1.5.1 与 netcoreapp1.0 不兼容。 - Paul Gorbas
@PaulGorbas 您说得对,该库没有更新到 .net core。您是在使用EF Core吗? - niaher
1
我的xUnit测试项目针对框架.NetCoreApp 1.0,也就是EF 7 b4更改命名约定之前的版本。 - Paul Gorbas

5

我虽然来晚了,但是发现这篇文章很有用:使用InMemory进行测试(MSDN文档)。

它解释了如何使用内存中的DB上下文(不是数据库),并且只需很少的编码就可以测试您的DBContext实现。


1
请注意,InMemory数据库默认情况下不是关系型数据库,因此不建议测试查询。 Sql-Lite内存提供程序可能更适合。 - Ross Brasseaux

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接