【问题标题】:How do I properly unit test a repository-pattern method, that has coupling to Entity Framework / DbContext?如何正确地对与实体框架/DbContext 耦合的存储库模式方法进行单元测试?
【发布时间】:2017-03-20 14:50:54
【问题描述】:

我是单元测试的新手,希望编写一个 N 层应用程序,因此我的 ORM 是从 BLL 中抽象出来的,中间有一个存储库模式。我将如何对与 EF (DbContext) 耦合的存储库模式方法进行单元测试?

我的 SUT 方法在这里:

public IList<Volunteer> GetAllVolunteers() {
    return dbctx.Volunteer.ToList();
}

到目前为止我的单元测试:

[Fact]
public void GetAllVolunteersMethodWorks() {
    // Arrange
    var fakeVolunteer = new Volunteer { Id = 0, Name = "Dummy" };
    var expected = new List<Volunteer>(new Volunteer[] { fakeVolunteer });

    var mockedCtxVolunteer = new Mock<DbSet<Volunteer>>();
    mockedCtxVolunteer.Setup(m => m.ToList()).Returns(expected);

    var mockedctx = new Mock<RSMContext>();
    mockedctx.Setup(m => m.Volunteer).Returns(mockedCtxVolunteer.Object);

    // Act
    var volunteerRepo = new VolunteerRepository(mockedctx.Object);
    var result = volunteerRepo.GetAllVolunteers();

    // Assert
    Assert.Equal(expected, result);
}

我收到一个错误 System.NotSupportedException: 'Expression references a method that does not contain the mocked object: m => m.ToList() 那么如何正确模拟 DbSet,以便我的假数据驻留在那里以正确测试我的 VolunteerRepository.GetAllVolunteers() 方法是否有效?

编辑(解决方案):

var mockedDbCtxVolunteer = new Mock<DbSet<Volunteer>>();
mockedDbCtxVolunteer.As<IQueryable<Volunteer>>().Setup(m => m.GetEnumerator()).Returns(expected.GetEnumerator());

【问题讨论】:

  • 我建议该方法不需要测试,因为您不是在测试自己的代码。
  • 好吧,在这个例子中,我的 GetAllVolunteers() 方法非常基本,但我的想法是我的 repo 在其他方法中会有更复杂的逻辑。所以让它发挥作用的想法以后会更有价值

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


【解决方案1】:

ToList() is an extension method,Moq(和其他库)cannot mock。不要担心模拟该方法,而是模拟它将使用什么:GetEnumerator():

var mockedCtxVolunteer = new Mock<DbSet<Volunteer>>();
mockedCtxVolunteer.Setup(m => m.GetEnumerator()).Returns(expected.GetEnumerator());

【讨论】:

    【解决方案2】:

    我们正在使用一个帮助类来模拟 DbSet。它为集合中的模拟项目使用内部数据存储。

    public class MockDbSet<T> : IDbSet<T> where T : class {
    
        readonly HashSet<T> _data;
        readonly IQueryable _query;
    
        public MockDbSet() : this(new HashSet<T>()) {            
        }
    
        public MockDbSet(params T[] entries) : this(new HashSet<T>(entries)) {
        }
    
        private MockDbSet(IEnumerable<T> data) {
            _data = new HashSet<T>(data);
            _query = _data.AsQueryable();
        }
    
        public T Add(T item) {
            _data.Add(item);
             return item;
        }
    
        public T Remove(T item) {
            _data.Remove(item);
            return item;
        }
    
        Type IQueryable.ElementType {
            get { return _query.ElementType; }
        }
    
        IEnumerator IEnumerable.GetEnumerator() {
            return _data.GetEnumerator();
        }
    
        // implement other members of IDbSet<T> ...
    }
    

    然后在您的单元测试中,使用 Moq 设置数据上下文。对Volunteer 集的访问将被重定向到模拟实现。

    var mockedVolunteer1 = new Volunteer { Name = "Uno" };
    var mockedDbSet = new MockDbSet<Volunteer> {
         mockedVolunteer1, mockedVolunteer2, mockedVolunteer3
    };
    
    var mockedContext = new Mock<MyDataContext>();
    mockedContext.Setup(ctx => ctx.Volunteer).Returns(mockedDbSet);
    

    使用 MockDbSet 类的优点是您不必猜测测试将访问哪些方法并因此需要模拟,而且您不必每次都设置模拟方法。

    【讨论】:

    • 现在我通过手动设置 GetEnumerator() 解决了我的问题,但我喜欢帮助类的想法,您在帖子中对此进行了演示。谢谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多