C# 如何在单元测试中实现EF的查找方法?

C# 如何在单元测试中实现EF的查找方法?,c#,linq,entity-framework,unit-testing,C#,Linq,Entity Framework,Unit Testing,我有一个WebAPI2.0项目,我正在进行单元测试。我的控制器有一个工作单元。工作单元包含许多用于各种数据库集的存储库。我在WebAPI中有一个Unity容器,我在测试项目中使用Moq。在各种存储库中,我使用实体框架的Find方法根据实体的键定位实体。此外,我使用的是实体框架6.0 以下是工作单元的一个非常普遍的示例: public class UnitOfWork { private IUnityContainer _container; public IUnityContai

我有一个WebAPI2.0项目,我正在进行单元测试。我的控制器有一个工作单元。工作单元包含许多用于各种数据库集的存储库。我在WebAPI中有一个Unity容器,我在测试项目中使用Moq。在各种存储库中,我使用实体框架的
Find
方法根据实体的键定位实体。此外,我使用的是实体框架6.0

以下是工作单元的一个非常普遍的示例:

public class UnitOfWork
{
    private IUnityContainer _container;
    public IUnityContainer Container
    {
        get
        {
            return _container ?? UnityConfig.GetConfiguredContainer();
        }
    }

    private ApplicationDbContext _context;    
    public ApplicationDbContext Context
    {
        get { _context ?? Container.Resolve<ApplicationDbContext>();  }
    }

    private GenericRepository<ExampleModel> _exampleModelRepository;
    public GenericRepository<ExampleModel> ExampleModelRepository
    {
        get { _exampleModelRepository ?? 
            Container.Resolve<GenericRepository<ExampleModel>>(); }
    }

    //Numerous other repositories and some additional methods for saving
}
我想我必须自定义我的代码,以便
TEntity
是一种具有Id属性的基类。这是我的理论,但我不确定这是最好的处理方法


所以我有两个问题。上述方法有效吗?如果不是,用
SingleOrDefault
方法覆盖
DbSet
中的
Find
方法的更好方法是什么?而且,这种方法只有在它们只有一个主键时才真正起作用。如果我的模型具有不同类型的复合键,该怎么办?我想我必须单独处理这些问题。好吧,那是三个问题

为了进一步阐述我之前的评论,我将从我提出的解决方案开始,然后解释原因

您的问题是:您的存储库依赖于
DbSet
。您无法有效地测试您的存储库,因为它们依赖于
DbSet.Find(int[])
,因此您决定用自己的
DbSet
变体
TestDbSet
。这是不必要的
DbSet
实现
IDbSet
。使用Moq,我们可以非常干净地创建这个接口的存根实现,它返回一个硬编码的值

class MyRepository
{
   public MyRepository(IDbSet<MyType> dbSet) 
   {
     this.dbSet = dbSet;
   }

   MyType FindEntity(int id)
   {
     return this.dbSet.Find(id);
   }
}
这将在实现中转化为如下内容:

class GenericDbSet<TKeyType,TObjectType> 
{        
  GenericDbSet(IDbSet<TObjectType> dbset)   
  {
    this.dbset = dbset;   
  }

  TObjectType Find(TKeyType key)   
  {
    // TODO: Convert key into something a regular dbset can understand
    return this.dbset(key);   
  } 
}
类genericdset
{        
通用数据库集(IDbSet数据库集)
{
this.dbset=dbset;
}
TObjectType查找(TKeyType键)
{
//TODO:将密钥转换为常规数据库集可以理解的内容
返回此.dbset(键);
} 
}

因此,基于该示例,我执行了以下操作,以便能够对我的UnitOfWork进行单元测试

1) 必须确保我的工作单元正在实现一个applicationdbcontext。(另外,当我说UnitOfWork时,我的控制器的UnitOfWork类型为IUnitOfWork。)

2) 我把所有的数据库集都单独放在我的IApplicationDbContext中了。当我注意到IDbSet没有包含RemoveAnge和FindAsync时,我选择了这种模式,我在代码中使用了这两种模式。另外,在EF6中,DbSet可以设置为虚拟,这在MSDN中是推荐的,因此是有意义的

3) 我按照示例创建TestDbContext和所有推荐的类(例如TestDbAsyncQueryProvider、TestDbAsyncEnumerable、TestDbAsyncEnumerator)。以下是代码:

public class TestContext : DbContext, IApplicationDbContext
{
    public TestContext()
    {

        this.ExampleModels= new TestBaseClassDbSet<ExampleModel>();
        //New up the rest of the TestBaseClassDbSet that are need for testing
        //Created an internal method to load the data
        _loadDbSets();
    }

    public virtual DbSet<ExampleModel> ExampleModels{ get; set; }
    //....List of remaining DbSets

    //Local property to see if the save method was called
    public int SaveChangesCount { get; private set; }

    //Override the SaveChanges method for testing
    public override int SaveChanges()
    {
        this.SaveChangesCount++;
        return 1;
    }
    //...Override more of the DbContext methods (e.g. SaveChangesAsync)

    private void _loadDbSets()
    {
        _loadExampleModels();
    }

    private void _loadExampleModels()
    {
        //ExpectedGlobals is a static class of the expected models 
        //that should be returned for some calls (e.g. GetById)
        this.ExampleModels.Add(ExpectedGlobal.Expected_ExampleModel);
    }
}
public类TestContext:DbContext,iaapplicationdbcontext
{
公共TestContext()
{
this.ExampleModels=newTestBaseClassDBSet();
//新建测试所需的TestBaseClassDbSet的其余部分
//创建了一个内部方法来加载数据
_loadDbSets();
}
公共虚拟数据库集ExampleModels{get;set;}
//..剩余数据库集列表
//本地属性,以查看是否调用了save方法
public int saveChangeScont{get;private set;}
//重写SaveChanges方法以进行测试
公共覆盖int SaveChanges()
{
这个.savechangesont++;
返回1;
}
//…覆盖更多DbContext方法(例如SaveChangesAsync)
私有void_loadDbSets()
{
_loadExampleModels();
}
私有void_loadExampleModels()
{
//ExpectedGlobals是预期模型的一个静态类
//对于某些调用(例如GetById),应该返回
this.ExampleModels.Add(ExpectedGlobal.Expected\u ExampleModel);
}
}
正如我在文章中提到的,我需要实现FindAsync方法,因此我添加了一个名为TestBaseClassDbSet的类,它是示例中TestDbSet类的一个修改。修改如下:

//BaseModel is a class that has a key called Id that is of type string
public class TestBaseClassDbSet<TEntity> : 
    DbSet<TEntity>
    , IQueryable, IEnumerable<TEntity>
    , IDbAsyncEnumerable<TEntity>
    where TEntity : BaseModel
{
    //....copied all the code from the TestDbSet class that was provided

    //Added the missing functions
    public override TEntity Find(params object[] keyValues)
    {
        var id = (string)keyValues.Single();
        return this.SingleOrDefault(b => b.Id == id);
    }

    public override Task<TEntity> FindAsync(params object[] keyValues)
    {
        var id = (string)keyValues.Single();
        return this.SingleOrDefaultAsync(b => b.Id == id);
    }
}
//BaseModel是一个类,它有一个名为Id的键,该键的类型为string
公共类TestBaseClassDbSet:
数据库集
,可数的,可数的
,可枚举
其中tenty:BaseModel
{
//..从提供的TestDbSet类复制了所有代码
//添加了缺少的函数
public override TEntity Find(参数对象[]键值)
{
var id=(字符串)keyValues.Single();
返回这个.SingleOrDefault(b=>b.Id==Id);
}
公共覆盖任务FindAsync(参数对象[]键值)
{
var id=(字符串)keyValues.Single();
返回这个.SingleOrDefaultAsync(b=>b.Id==Id);
}
}
4) 创建了TestContext的实例,并将其传递到我的模拟中

var context = new TestContext();
var userStore = new Mock<IUserStore<ApplicationUser>>();

//ExpectedGlobal contains a static variable call Expected_User
//to be used as to populate the principle
// when mocking the HttpRequestContext
userStore
    .Setup(m => m.FindByIdAsync(ExpectedGlobal.Expected_User.Id))
    .Returns(Task.FromResult(ExpectedGlobal.Expected_User));

var mockUserManager = new Mock<ApplicationUserManager>(userStore.Object);

var mockUnitOfWork = 
         new Mock<IUnitOfWork>(mockUserManager.Object, context) 
         { CallBase = false };
var context=newtestcontext();
var userStore=new Mock();
//ExpectedGlobal包含预期用户的静态变量调用
//用于填充原理
//模拟HttpRequestContext时
用户商店
.Setup(m=>m.FindByIdAsync(ExpectedGlobal.Expected\u User.Id))
.Returns(Task.FromResult(ExpectedGlobal.Expected_User));
var mockUserManager=newmock(userStore.Object);
var mockUnitOfWork=
新建Mock(mockUserManager.Object,上下文)
{CallBase=false};
然后我将mockUnitOfWork注入控制器,瞧。这个实现似乎工作得很完美。也就是说,根据我在网上读到的一些提要,它可能会被一些开发人员仔细检查,但我希望其他一些人会发现这是有用的


~Cheers

我意识到这是一个老问题,但在我自己为单元测试模拟数据时遇到这个问题后,我编写了这个“Find”方法的通用版本,可用于TestDBSet实现中,如上所述

使用此方法意味着您不必为其创建具体类型
public class TestContext : DbContext, IApplicationDbContext
{
    public TestContext()
    {

        this.ExampleModels= new TestBaseClassDbSet<ExampleModel>();
        //New up the rest of the TestBaseClassDbSet that are need for testing
        //Created an internal method to load the data
        _loadDbSets();
    }

    public virtual DbSet<ExampleModel> ExampleModels{ get; set; }
    //....List of remaining DbSets

    //Local property to see if the save method was called
    public int SaveChangesCount { get; private set; }

    //Override the SaveChanges method for testing
    public override int SaveChanges()
    {
        this.SaveChangesCount++;
        return 1;
    }
    //...Override more of the DbContext methods (e.g. SaveChangesAsync)

    private void _loadDbSets()
    {
        _loadExampleModels();
    }

    private void _loadExampleModels()
    {
        //ExpectedGlobals is a static class of the expected models 
        //that should be returned for some calls (e.g. GetById)
        this.ExampleModels.Add(ExpectedGlobal.Expected_ExampleModel);
    }
}
//BaseModel is a class that has a key called Id that is of type string
public class TestBaseClassDbSet<TEntity> : 
    DbSet<TEntity>
    , IQueryable, IEnumerable<TEntity>
    , IDbAsyncEnumerable<TEntity>
    where TEntity : BaseModel
{
    //....copied all the code from the TestDbSet class that was provided

    //Added the missing functions
    public override TEntity Find(params object[] keyValues)
    {
        var id = (string)keyValues.Single();
        return this.SingleOrDefault(b => b.Id == id);
    }

    public override Task<TEntity> FindAsync(params object[] keyValues)
    {
        var id = (string)keyValues.Single();
        return this.SingleOrDefaultAsync(b => b.Id == id);
    }
}
var context = new TestContext();
var userStore = new Mock<IUserStore<ApplicationUser>>();

//ExpectedGlobal contains a static variable call Expected_User
//to be used as to populate the principle
// when mocking the HttpRequestContext
userStore
    .Setup(m => m.FindByIdAsync(ExpectedGlobal.Expected_User.Id))
    .Returns(Task.FromResult(ExpectedGlobal.Expected_User));

var mockUserManager = new Mock<ApplicationUserManager>(userStore.Object);

var mockUnitOfWork = 
         new Mock<IUnitOfWork>(mockUserManager.Object, context) 
         { CallBase = false };
public override T Find(params object[] keyValues)
{
    ParameterExpression _ParamExp = Expression.Parameter(typeof(T), "a");

    Expression _BodyExp = null;

    Expression _Prop = null;
    Expression _Cons = null;

    PropertyInfo[] props = typeof(T).GetProperties();
    var typeName = typeof(T).Name.ToLower() + "id";
    var key = props.Where(p => (p.Name.ToLower().Equals("id")) || (p.Name.ToLower().Equals(typeName))).Single();


    _Prop = Expression.Property(_ParamExp, key.Name);
    _Cons = Expression.Constant(keyValues.Single(), key.PropertyType);

    _BodyExp = Expression.Equal(_Prop, _Cons);

    var _Lamba = Expression.Lambda<Func<T, Boolean>>(_BodyExp, new ParameterExpression[] { _ParamExp });

    return this.SingleOrDefault(_Lamba);

}