使用 Moq 模仿 EF 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
142918 次浏览

I managed to solve it by creating a FakeDbSet<T> class that implements 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();
}
}

Now my test looks like this:

[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());
}

If anyone is still looking for answers I've implemented a small library to allow mocking DbContext.

step 1

Install Coderful.EntityFramework.Testing nuget package:

Install-Package Coderful.EntityFramework.Testing

step 2

Then create a class like this:

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

step 3

Now you can create mocks super easily:

// 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;

And then use your mock:

// 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();

Full article: http://www.22bugs.co/post/Mocking-DbContext/

Thank you Gaui for your great idea =)

I did add some improvements to your solution and want to share it.

  1. My FakeDbSet also inherents from DbSet to get additional methods like AddRange()
  2. I replaced the ObservableCollection<T> with List<T> to pass all the already implemented methods in List<> up to my FakeDbSet

My 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();
}
}

It is very easy to modify the dbSet and Mock the EF Context Object:

    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);

Now it is possible to execute Linq queries, but be a aware that foreign key references may not be created automatically:

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

Because the context object is mocked the Context.SaveChanges() won't do anything and property changes of your entites might not be populated to your dbSet. I solved this by mocking my SetModifed() method to populate the changes.

In case anyone is still interested, I was having the same problem and found this article very helpful: Entity Framework Testing with a Mocking Framework (EF6 onwards)

It only applies to Entity Framework 6 or newer, but it covers everything from simple SaveChanges tests to async query testing all using Moq (and a few of manual classes).

Based on this MSDN article, I've created my own libraries for mocking DbContext and DbSet:

  • EntityFrameworkMock - GitHub
  • EntityFrameworkMockCore - GitHub

Both available on NuGet and GitHub.

The reason I've created these libraries is because I wanted to emulate the SaveChanges behavior, throw a DbUpdateException when inserting models with the same primary key and support multi-column/auto-increment primary keys in the models.

In addition, since both DbSetMock and DbContextMock inherit from Mock<DbSet> and Mock<DbContext>, you can use all features of the Moq framework.

Next to Moq, there also is an NSubstitute implementation.

Usage with the Moq version looks like this:

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

I'm late, but found this article helpful: Testing with InMemory (MSDN Docs).

It explains how to use an in memory DB context (which is not a database) with the benefit of very little coding and the opportunity to actually test your DBContext implementation.