模拟 AsNoTracking Entity Framework

34

如何模拟AsNoTracking方法?
在下面的例子中,DbContext已注入到服务类中。如果我从GetOrderedProducts方法中移除AsNoTracking扩展方法,它可以正常工作,但是使用AsNoTracking时测试失败,因为它返回null。我也尝试过模拟AsNoTracking以返回正确的值,但没有成功。

public interface IUnitOfWork
{
    IDbSet<TEntity> Set<TEntity>() where TEntity : class;
    int SaveAllChanges();
}

public class Entites : DbContext, IUnitOfWork
{
    public virtual DbSet<Product> Products { get; set; }  // This is virtual because Moq needs to override the behaviour

    public new virtual IDbSet<TEntity> Set<TEntity>() where TEntity : class   // This is virtual because Moq needs to override the behaviour 
    {
        return base.Set<TEntity>();
    }

    public int SaveAllChanges()
    {
        return base.SaveChanges();
    }
}

    public class ProductService
{
    private readonly IDbSet<Product> _products;
    private readonly IUnitOfWork _uow;

    public ProductService(IUnitOfWork uow)
    {
        _uow = uow;
        _products = _uow.Set<Product>();
    }
    public IEnumerable<Product> GetOrderedProducts()
    {
        return _products.AsNoTracking().OrderBy(x => x.Name).ToList();
    }
}

    [TestFixture]
public class ProductServiceTest
{
    private readonly ProductService _productService;

    public ProductServiceTest()
    {
        IQueryable<Product> data = GetRoadNetworks().AsQueryable();
        var mockSet = new Mock<DbSet<Product>>();
        mockSet.As<IQueryable<Product>>().Setup(m => m.Provider).Returns(data.Provider);
        mockSet.As<IQueryable<Product>>().Setup(m => m.Expression).Returns(data.Expression);
        mockSet.As<IQueryable<Product>>().Setup(m => m.ElementType).Returns(data.ElementType);
        mockSet.As<IQueryable<Product>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
        var context = new Mock<Entites>();
        context.Setup(c => c.Products).Returns(mockSet.Object);
        context.Setup(m => m.Set<Product>()).Returns(mockSet.Object);
        context.Setup(c => c.Products.AsNoTracking()).Returns(mockSet.Object);
        _productService = new ProductService(context.Object);
    }

    private IEnumerable<Product> GetRoadNetworks()
    {
        return new List<Product>
        {
            new Product
            {
                Id = 1,
                Name = "A"
            },
            new Product
            {
                Id = 2,
                Name = "B"
            },
            new Product
            {
                Id = 1,
                Name = "C"
            }
        };
    }

    [Test]
    public void GetOrderedProductTest()
    {
        IEnumerable<Product> products = _productService.GetOrderedProducts();
        List<string> names = products.Select(x => x.Name).ToList();
        var expected = new List<string> {"A", "B", "C"};
        CollectionAssert.AreEqual(names, expected);
    }
}

问题是AsNoTracking在单元测试中返回null。enter image description here


这些注释表明应该返回除 null 以外的其他内容:http://msdn.microsoft.com/en-us/library/gg679352%28v=vs.103%29.aspx - ta.speot.is
@ta.speot.is 没错,但它返回空值。 - Shahin
我已经在这里回答了相同的问题:链接 - arakakivl
3个回答

70

查看 AsNoTracking() 扩展方法的源代码

public static IQueryable AsNoTracking(this IQueryable source)
{
    var asDbQuery = source as DbQuery;
    return asDbQuery != null ? asDbQuery.AsNoTracking() : CommonAsNoTracking(source);
}

由于您尝试模拟的source(您的DbSet<Product>)确实是一个DbQuery(因为DbSet派生自DbQuery),它尝试调用“真正的”(非模拟的)AsNoTracking()方法,该方法正确地返回null。

尝试同时模拟AsNoTracking()方法:

mockSet.Setup(x => x.AsNoTracking()).Returns(mockSet.Object);

4
这在dotnet/EF core上无法正常工作:表达式引用了一个不属于模拟对象的方法:x => x.AsNoTracking<Tags>()。我猜测是因为AsNoTracking()不再属于IQueryable,而是属于'Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions'?你有解决方案吗? - Bassebus
@Bassebus,我对EF Core不够熟悉,无法回答这个问题,但它可能值得提出一个新问题。 - haim770
好的,谢谢。我又做了一些谷歌搜索,认为这可能是问题/解决方案 https://github.com/aspnet/EntityFramework/issues/7937 - Bassebus
那么你最终使用了什么解决方案? - Paul Gorbas
这里的mockSet是什么类型? - Triynko
显示剩余2条评论

6

你有:

context.Setup(c => c.Products).Returns(mockSet.Object);
context.Setup(m => m.Set<Product>()).Returns(mockSet.Object);
context.Setup(c => c.Products.AsNoTracking()).Returns(mockSet.Object);

但请记住,扩展方法只是语法糖。因此:
c.Products.AsNoTracking()

真正的意思只是:

System.Data.Entity.DbExtensions.AsNoTracking(c.Products)

因此,您上面的模拟设置是无意义的。
问题是静态 DbExtensions.AsNoTracking(source) 方法 对其参数实际执行了什么操作。另请参见 线程 AsNoTracking() 有什么区别? 如果从测试类中删除涉及 AsNoTrackingSetup,会发生什么?
为所有模拟对象提供 MockBehavior.Strict 可能会有所帮助。在这种情况下,您将发现静态方法调用它们的成员是否可以由Moq进行模拟(即一般意义上的虚拟方法/属性)。如果需要,也许可以模拟非静态 方法DbQuery.AsNoTracking

谢谢,我需要使用AsNoTracking来提高查询性能。当我在服务类中使用AsNoTracking时,我正在寻找解决空异常问题的方法。 - Shahin
在我的 EF(版本 6.0)中,它不总是一个扩展方法,而是在 DbQuery<> 中定义的虚拟方法。只有当您将数据引用为 IQueryable<> 时,才必须使用扩展方法。 - haim770
1
@haim770,你说得完全正确。你的答案比我的更精确,尽管我在最后一句中暗示了DbQuery<>上的非静态方法。我要重申的是,如果他使用了Strict模拟,他会得到一个异常,告诉他需要模拟哪个方法,而不是等到出现NullReferenceException。人们太多地使用松散模拟了。 - Jeppe Stig Nielsen
@JeppeStigNielsen,谢谢。我不太了解严格模拟和松散模拟。但是出于好奇,如果他使用了严格模拟,我们什么时候可以期望抛出异常呢? - haim770
使用严格模拟,一旦调用了不是“Setup”的成员,就会抛出异常,指出“所有调用必须有相应的设置”。因此,如果类型T有一个方法int Meth(),并且Meth足够虚拟,以至于Moq可以控制其实现,则示例为:var m = new Mock<T>(MockBehavior.Strict); /* no setup for Meth */ m.Object.Meth(); /* throws an exception */与松散模拟相比,这将简单地返回default(int)0。(在问题的情况下,它返回了default(DbQuery)null。) - Jeppe Stig Nielsen
@JeppeStigNielsen,我明白了。谢谢。 - haim770

-2
你可以使用Entity Framework Effort来模拟AsNoTracking(),也可以使用Effort模拟Db事务和实体状态 - Effort官网

仅提供链接的答案并不实用。建议删除以节约读者们重复阅读此信息所消耗的时间。 - No Refunds No Returns

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