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

Rog*_*ala 5 c# linq unit-testing entity-framework

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

我遇到的问题是我Find在存储库中使用该方法进行一些LINQ查询.基于这篇文章,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); 
        } 
    } 
}
Run Code Online (Sandbox Code Playgroud)

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

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

Dan*_*try 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);
   }
}
Run Code Online (Sandbox Code Playgroud)

通过将依赖关系切换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);
}
Run Code Online (Sandbox Code Playgroud)

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

另外,如果您发现自己正在测试DbContext- 请不要.如果你必须这样做,那么你DbContext的筹码太多了,如果你试图离开Entity Framework就会受到伤害.创建一个接口,公开您需要的功能DbContext并使用它.

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

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

interface IGenericDbSet<TKeyType, TObjectType>
{
  TObjectType Find(TKeyType keyType);
}
Run Code Online (Sandbox Code Playgroud)

然后,这将在实现中翻译成类似于:

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);   
  } 
}
Run Code Online (Sandbox Code Playgroud)