在ASP.NET Core中模拟或更好地解决AsNoTracking

时间:2018-10-24 21:37:41

标签: c# linq unit-testing asp.net-core

您如何模拟AsNoTracking或此问题有更好的解决方法?

示例:

public class MyContext : MyContextBase
  {
    // Constructor
    public MyContext(DbContextOptions<MyContext> options) : base(options)
    {
    }

    // Public properties
    public DbSet<MyList> MyLists{ get; set; }
  }

public class MyList
{
    public string Id { get; set; }
    public string Email { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public bool Blocked { get; set; }
}


public class MyController : MyControllerBase
{ 
    private MyContext ContactContext = this.ServiceProvider.GetService<MyContext>();

    public MyController(IServiceProvider serviceProvider) : base(serviceProvider)
    {
    }

    private bool isContact(string firstName, string lastName)
    {
      try
      {
        var list = this
          .ContactContext
          .MyLists
          .AsNoTracking()  // !!!Here it explodes!!!
          .FirstOrDefault(entity => entity.FirstName == firstName && entity.LastName == lastName);
        return list != null;
      }
      catch (Exception exception)
      {
        throws Exception;
      }
      return false;
    }
}

我的测试:

using Moq;
using Xunit;

[Fact]
[Trait("Category", "Controller")]
public void Test()
{
  string firstName = "Bob";
  string lastName = "Baumeister";

  // Creating a list with the expectad data
  var fakeContacts = new MyList[]
  {
    new MyList() { FirstName = "Ted", LastName = "Teddy" },
    new MyList() { PartnerId = "Bob", Email = "Baumeister" }
  };
  // Mocking the DbSet<MyList>
  var dbSet = CreateMockSet(fakeContacts.AsQueryable());
  // Setting the mocked dbSet in ContactContext
  ContactContext contactContext = new ContactContext(new DbContextOptions<ContactContext>())
  {
    MyLists = dbSet.Object
  };
  // Mocking ServiceProvider
  serviceProvider
    .Setup(s => s.GetService(typeof(ContactContext)))
    .Returns(contactContext);
  // Creating a controller
  var controller = new ContactController(serviceProvider.Object);

  // Act
  bool result = controller.isContact(firstName, lastName)

  // Assert
  Assert.True(result);
}

private Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> data)
  where T : class
{
  var queryableData = data.AsQueryable();
  var mockSet = new Mock<DbSet<T>>();
  mockSet.As<IQueryable<T>>().Setup(m => m.Provider)
    .Returns(queryableData.Provider);
  mockSet.As<IQueryable<T>>().Setup(m => m.Expression)
    .Returns(queryableData.Expression);
  mockSet.As<IQueryable<T>>().Setup(m => m.ElementType)
    .Returns(queryableData.ElementType);
  mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator())
    .Returns(queryableData.GetEnumerator());
  return mockSet;
}

每次运行此测试时,AsNoTracking()处的isContact(String firstName,String lastName)引发的异常是:

Exception.Message:

  

类型上没有方法“ AsNoTracking”   'Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions'   匹配指定的参数

Exception.StackTrace:

at System.Linq.EnumerableRewriter.FindMethod(Type type, String name, ReadOnlyCollection'1 args, Type[] typeArgs) 
at System.Linq.EnumerableRewriter.VisitMethodCall(MethodCallExpression m) 
at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor) 
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) 
at System.Linq.EnumerableQuery'1.GetEnumerator() 
at System.Linq.EnumerableQuery'1.System.Collections.Generic.IEnumerable<T>.GetEnumerator() 
at My.Package.Contact.Controller.MyController.isContact(String firstName, String lastName) in C:\Users\source\repos\src\My.Package\My.Package.Contact\Controller\MyController.cs:line 31

我的尝试:

尝试像stackoverflow: mock-asnotracking-entity-framework中所建议的那样模拟AsNoTracking:

mockSet.As<IQueryable<T>>().Setup(m => m.AsNoTracking<T>())
    .Returns(mockSet.Object);

导致System.NotSupportedException中出现ASP.NET Core:

  

'扩展方法上的无效设置:m => m.AsNoTracking()'   mockSet.Setup(m => m.AsNoTracking())       .Returns(mockSet.Object);

在AtNoTracking()更好地了解了Microsoft.EntityFrameworkCore EntityFrameworkQueryableExtensions EntityFrameworkCore EntityFrameworkQueryableExtensions.cs之后:

public static IQueryable<TEntity> AsNoTracking<TEntity>(
            [NotNull] this IQueryable<TEntity> source)
            where TEntity : class
        {
            Check.NotNull(source, nameof(source));

            return
                source.Provider is EntityQueryProvider
                    ? source.Provider.CreateQuery<TEntity>(
                        Expression.Call(
                            instance: null,
                            method: AsNoTrackingMethodInfo.MakeGenericMethod(typeof(TEntity)),
                            arguments: source.Expression))
                    : source;
}

由于我在测试过程中提供了模拟的DbSet <>,因此Provider是可查询的,因为“ source.Provider是EntityQueryProvider”为false,因此函数AsNoTracking应该返回输入源。

我唯一无法检查的是Check.NotNull(source,nameof(source));既然我找不到它做什么?如果有人对它的解释或代码说明了它的作用,如果可以与我分享,我将不胜感激。

解决方法:

我在互联网上找到的唯一解决方法是来自线程https://github.com/aspnet/EntityFrameworkCore/issues/7937中的@cdwaddell,该线程基本上编写了自己的门控版本的AsNoTracking()。使用变通办法可以成功,但是我不想实施它,因为它似乎没有检查任何东西?

public static class QueryableExtensions
{
    public static IQueryable<T> AsGatedNoTracking<T>(this IQueryable<T> source) where T : class
    {
      if (source.Provider is EntityQueryProvider)
        return source.AsNoTracking<T>();
      return source;
    }
}

所以,我的问题:

  1. 解决方法是否是测试此类内容的唯一方法?
  2. 可以模拟这个吗?
  3. Check.NotNull(source,nameof(source))是什么?在     AsNoTracking()可以吗?

1 个答案:

答案 0 :(得分:2)

请勿嘲笑DataContext。

DataContext是访问层的实现细节。 Entity Framework Core提供了两个选项来编写具有DataContext依赖关系的测试,而无需实际的数据库。

内存数据库-Testing with InMemory

SQLite内存-Testing with SQLite

为什么不应该模拟DataContext?
仅仅因为使用模拟DataContext,您将仅测试按预期顺序调用的方法。
相反,在测试中,您应该测试代码的行为,返回的值,更改的状态(数据库更新)。
测试行为时,您将能够重构/优化代码,而无需为代码中的每个更改重写测试。

如果内存中测试未提供所需的行为,请针对实际数据库测试代码。