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

时间:2015-06-25 14:59:46

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

我有一个Web API 2.0项目,我正在进行单元测试。我的控制器有一个工作单元。工作单元包含许多用于各种DbSet的存储库。我在Web API中有一个Unity容器,我在测试项目中使用Moq。在各种存储库中,我使用实体框架的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: Testing with your own test doubles (EF6 onwards),我必须创建一个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属性的基类的类型。这是我的理论,但我不确定这是处理这个问题的最好方法。

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

3 个答案:

答案 0 :(得分:5)

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

您的问题是:您的存储库依赖于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>版本。

另一方面,如果您发现自己正在测试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);   
  } 
}

答案 1 :(得分:3)

我意识到这是一个老问题,但是在我为单元测试模拟数据时遇到这个问题后,我写了这个&#39; Find&#39;可以在msdn

中解释的TestDBSet实现中使用的方法

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

  1. &#39;标识&#39;
  2. &#39; ID&#39;
  3. &#39; ID&#39;
  4. classname +&#39; id&#39;
  5. classname +&#39; Id&#39;
  6. classname +&#39; ID&#39;

    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);
    
    }
    
  7. 同样从性能的角度来看,它不会像推荐的方法一样快,但对于我的目的来说还不错。

答案 2 :(得分:0)

基于这个例子,我做了以下工作,以便能够对UnitOfWork进行单元测试。

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

2)我单独留下了IApplicationDbContext中的所有DbSet。一旦我注意到IDbSet不包含我在整个代码中使用的RemoveRange和FindAsync,我就选择了这个模式。此外,使用EF6,DbSet可以设置为虚拟,这在MSDN中是推荐的,所以这是有道理的。

3)我按照Creating the in-memory test doubles 示例创建了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实例并将其传递给我的模拟。

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注入控制器,瞧。这种实现似乎是完美的。也就是说,基于我在线阅读的一些提要,它可能会被一些开发人员仔细审查,但我希望其他人认为这有用。

〜干杯