繁体   English   中英

如何在单元测试中实现EF的FIND方法?

[英]How to implement FIND method of EF in Unit Test?

我有一个Web API 2.0项目,我正在进行单元测试。 我的控制器有一个工作单元。 工作单元包含许多用于各种DbSet的存储库。 我在Web API中有一个Unity容器,我在测试项目中使用Moq。 在各种存储库中,我使用Entity Framework的Find方法根据它的密钥定位实体。 此外,我正在使用Entity Framework 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
}

我遇到的问题是我对存储库中的一些LINQ查询使用了Find方法。 基于这篇文章, MSDN:使用您自己的测试加倍测试(EF6以上) ,我必须创建一个TestDbSet<ExampleModel>来测试Find方法。 我在考虑将代码定制为这样的代码:

namespace TestingDemo 
{ 
    class TestDbSet : TestDbSet<TEntity> 
    { 
        public override TEntity Find(params object[] keyValues) 
        { 
            var id = (string)keyValues.Single(); 
            return this.SingleOrDefault(b => b.Id == id); 
        } 
    } 
}

我想我必须自定义我的代码,以便TEntity是一种具有Id属性的基类的类型。 这是我的理论,但我不确定这是处理这个问题的最好方法。

所以我有两个问题。 上面列出的方法是否有效? 如果没有,使用SingleOrDefault方法覆盖DbSetFind方法会有什么更好的方法? 此外,这种方法只有在它们只有一个主键时才能真正起作用。 如果我的模型有不同类型的复合键怎么办? 我认为我必须单独处理这些。 好的,那是三个问题?

为了扩展我之前的评论,我将从我提出的解决方案开始,然后解释原因。

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

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

   MyType FindEntity(int id)
   {
     return this.dbSet.Find(id);
   }
}

通过将依赖关系从DbSet<T>切换到IDbSet<T> ,测试现在看起来像这样:

public void MyRepository_FindEntity_ReturnsExpectedEntity()
{
  var id = 5;
  var expectedEntity = new MyType();
  var dbSet = Mock.Of<IDbSet<MyType>>(set => set.Find(It.is<int>(id)) === expectedEntity));
  var repository = new MyRepository(dbSet);

  var result = repository.FindEntity(id);

  Assert.AreSame(expectedEntity, result);
}

那里 - 一个干净的测试,不暴露任何实现细节或处理具体类的讨厌IDbSet<MyType>并允许您替换自己的IDbSet<MyType>版本。

另外,如果您发现自己正在测试DbContext - 请不要。 如果你必须这样做,你的DbContext在堆栈上太远了,如果你试图离开实体框架,它会受到伤害。 创建一个接口,从DbContext公开您需要的功能并使用它。

注意 :我上面使用了Moq。 你可以使用任何模拟框架,我只是喜欢Moq。

如果你的模型有一个复合键(或者具有不同类型键的能力),那么事情会变得有点棘手。 解决这个问题的方法是引入自己的界面。 这个接口应该由您的存储库使用,并且实现应该是一个适配器,用于将密钥从您的复合类型转换为EF可以处理的内容。 你可能会用这样的东西:

interface IGenericDbSet<TKeyType, TObjectType>
{
  TObjectType Find(TKeyType keyType);
}

然后,这将在实现中转化为类似于:

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);   
  } 
}

我意识到这是一个老问题,但是在我为单元测试模拟数据时遇到这个问题后,我编写了这个'Find'方法的通用版本,可以在msdn上解释的TestDBSet实现中使用

使用此方法意味着您不必为每个DbSet创建具体类型。 需要注意的一点是,如果您的实体具有以下某种形式的主键,则此实现有效(确保您可以轻松修改以适应其他表单):

  1. 'ID'
  2. 'ID'
  3. 'ID'
  4. classname +'id'
  5. classname +'Id'
  6. classname +'ID'

     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); } 

从性能的角度来看,它不会像推荐的方法一样快,但对于我的目的来说它很好。

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

1)必须确保我的UnitOfWork正在实现IApplicationDbContext。 (另外,当我说UnitOfWork时,我的控制器的UnitOfWork是IUnitOfWork类型。)

2)我单独留下了IApplicationDbContext中的所有DbSet。 一旦我注意到IDbSet不包含我在整个代码中使用的RemoveRange和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);
    }
}

正如我在帖子中提到的,我需要实现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);
    }
}

4)创建了一个TestContext实例并将其传递给我的Mock。

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 };

然后我将mockUnitOfWork注入控制器,瞧。 这种实现似乎是完美的。 也就是说,基于我在线阅读的一些提要,它可能会被一些开发人员仔细检查,但我希望其他人认为这有用。

〜干杯

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM