Moq和Entity Framework

4
我想找到最好的方法来做这件事 - 我已经花了几个小时进行了大量搜索,但无法使其工作。我需要一双新鲜的眼睛和角度。
我正在尝试创建一个简单的应用程序。 它将使用EF 6.1.0作为数据访问层(DAL)。
我有一个名为User的实体。 我还有另一个名为WorkItem的实体。
一个用户可以有多个WorkItems。
我已经创建了一个名为“TimesheetsContext”的EFDbContext。 它继承自DbContext并具有两个虚拟属性:
    public virtual IDbSet<WorkItem> WorkItems { get; set; }

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

我还有一个IUnitOfWork接口(以及它的具体实现类)

public interface IUnitOfWork
{
    ITimeSheetContext Context { get; set; }
    IWorkItemRepository WorkItemRepository { get; set;  }
    IUserRepository UserRepository { get; set; }
}

WorkItem和User库仅具有Add、Login、Update等功能,这些功能又调用EfDBContext来执行请求操作。

现在,我想创建一个单元测试,以便模拟DbContext并能够添加用户或工作项。但是,我似乎无法使其正常工作。

一旦有了你的帮助,我就可以轻松地将其改为使用服务层,并传入IUnitOfWork。

使用Moq和EntityFramework.Testing.Moq,我无法使其正常工作:

    private Mock<TimeSheetContext> _mockContext;

    // <snip />

        Guid userId1 = Guid.NewGuid();
        Guid userId2 = Guid.NewGuid();
        Guid userId3 = Guid.NewGuid();
        var users = new List<User>(new[] 
        { 
            new User { Firstname = "Joe", Lastname = "Bloggs", Password = "pass1", UserId = userId1, Username = "JoeBloggs" },
            new User { Firstname = "Thom", Lastname = "Stevens", Password = "pass2", UserId = userId2, Username = "ThomStevens"},
            new User { Firstname = "Homer", Lastname = "Simpson", Password = "pass3", UserId = userId3, Username = "HomerSimpson" }
        }).AsQueryable();

        var tasks = new List<LionTask>(new[] 
        { 
            new WorkItem { CreatedDate = DateTime.Today, Description = "Desc1", ModifiedDate = DateTime.Today, State = 1, TaskId = Guid.NewGuid(), Title = "Test Title", UserId = userId1 },
            new WorkItem { CreatedDate = DateTime.Today, Description = "Desc2", ModifiedDate = DateTime.Today, State = 2, TaskId = Guid.NewGuid(), Title = "Test Title 2", UserId = userId2 },
            new WorkItem { CreatedDate = DateTime.Today, Description = "Desc3", ModifiedDate = DateTime.Today, State = 3, TaskId = Guid.NewGuid(), Title = "Test Title 3", UserId = userId3 }
        }).AsQueryable();

        this._mockContext = new Mock<TimeSheetContext>();
        var taskDbSetMock = new Mock<IDbSet<WorkItem>>();
        var userDbSetMock = new Mock<IDbSet<User>>();
        userDbSetMock.As<IQueryable<User>>().Setup(m => m.Provider).Returns(users.Provider);
        userDbSetMock.As<IQueryable<User>>().Setup(m => m.Expression).Returns(users.Expression);
        userDbSetMock.As<IQueryable<User>>().Setup(m => m.ElementType).Returns(users.ElementType);
        userDbSetMock.As<IQueryable<User>>().Setup(m => m.GetEnumerator()).Returns(users.GetEnumerator());

        taskDbSetMock.As<IQueryable<WorkItem>>().Setup(m => m.Provider).Returns(tasks.Provider);
        taskDbSetMock.As<IQueryable<WorkItem>>().Setup(m => m.Expression).Returns(tasks.Expression);
        taskDbSetMock.As<IQueryable<WorkItem>>().Setup(m => m.ElementType).Returns(tasks.ElementType);
        taskDbSetMock.As<IQueryable<WorkItem>>().Setup(m => m.GetEnumerator()).Returns(tasks.GetEnumerator());

        this._mockContext.Setup(c => c.Users).Returns(userDbSetMock.Object);
        this._mockContext.Setup(c => c.WorkItems).Returns(taskDbSetMock.Object);

最后,我有一个像这样的测试,但是当我添加用户时,仍然返回3,断言失败:
        User u = new User { Firstname = "Timmy", Lastname = "Johanson", Password = "omg123", UserId = Guid.NewGuid(), Username = "TJ" };
        this._mockContext.Object.Users.Add(u);

        Assert.AreEqual(4, this._mockContext.Object.Users.Count());

我这样做是错的吗?


发生了什么事情? - John Saunders
@JohnSaunders - 当我添加一个新的用户项目时,上下文仍然包含3个项目而不是4个。我希望在调用模拟上下文/存储库时能够持久化并查看任何更新/更改。 - Ahmed ilyas
这可能会非常吸引你:http://vannevel.net/blog/2015/02/26/11/ - Jeroen Vannevel
2个回答

1

看起来你正在尝试将users设置为后备存储,但这不是Moq的工作方式。模拟对象返回的是你告诉它要返回的内容:

userDbSetMock.As<IQueryable<User>>().Setup(m => m.GetEnumerator())
  .Returns(users.GetEnumerator());

如果你写成了以下内容:
users.Add(u);

users.GetEnumerator()会返回你预期的四个用户。

然而,这并不清楚如何帮助您测试接受模拟上下文的对象。我认为您应该重新审视被测试的主题是如何被测试的,您可能会发现在测试过程中不需要添加此对象。


谢谢。明白了。那么,我想测试模拟上下文并确保它可以添加/删除/更新项目。那么,我该如何修改当前的测试设置以使我能够从测试方法本身执行此操作?所有的模拟都是在 [TestInitialize] 的一个方法中完成的,在测试运行之前。 - Ahmed ilyas
为什么要测试模拟上下文?这不是在测试Moq的工作原理吗?存根旨在测试静态上下文。对于动态上下文,您可能希望考虑创建一个自定义的虚拟上下文类,并使用List作为后备存储。 - neontapir
以下是另一个问题中的这种对象的示例:https://dev59.com/OGgt5IYBdhLWcg3w0wzB - neontapir

1

由于你已经将所有内容都mocked out,因此不清楚你要测试的具体类是什么。

如果你想测试你的ContextClass(在我看来似乎只是在测试通常不被允许的第三方代码),那么你需要使用实际访问数据库的集成测试。

最可能的情况是你想要某种IRepository,其中包含了一个mocked TimesheetsContext

public interface ITimesheetsContext
{
    IDbSet<Timesheet> Timesheets { get; set; }
}
public interface ITimesheetRepository
{
    void Add(Timesheet sheet);
}

public class DbTimesheetRepository : ITimesheetRepository
{
    public ITimesheetsContext _context;

    public DbTimesheetRepository(ITimesheetsContext context)
    {
        _context = context;
    }

    public void Add(Timesheet ts)
    {
        _context.Timesheets.Add(ts);
    }
}

  [TestFixture]
  public class DbTimesheetRepositoryTests
  {
      public void Add_CallsSetAdd()
      {
          // Arrange
          var timesheet = new Timesheet();

          var timesheetsMock = new Mock<IDbSet<Timesheet>>();
          timesheetsMock.Setup(t => t.Add(timesheet)).Verifiable();

          var contextMock = new Mock<ITimesheetsContext>();
          contextMock.Setup(x => x.Timesheets).Returns(timesheetsMock.Object);

          var repo = new DbTimesheetRepository(contextMock.Object);

          // Act
          repo.Add(timesheet);

          // Assert
          timesheetsMock.Verify(t => t.Add(timesheet), Times.Once);
      }          
  }

未来您可以拥有一个

public class ServiceTimesheetContext : ITimesheetContext { }

它会访问服务而不是数据库


谢谢。这确实有道理。我想我试图让它达到能够模拟一个服务并调用其方法的程度,但是支持存储的内部集合而不是数据库,但不确定如何使用IUnitOfWork / EfDBContext实现。 - Ahmed ilyas
那么让我提出另一个问题:我该如何模拟服务层,其中构造函数需要IUnitOfWork接口(该接口又具有IUserRepository和IWorkItemRepository属性)?我如何能够调用这些调用并能够在内存中持久化数据以进行单元测试? - Ahmed ilyas

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