【问题标题】:How to implement FIND method of EF in Unit Test?如何在单元测试中实现EF的FIND方法?
【发布时间】:2015-06-25 14:59:45
【问题描述】:

我有一个正在单元测试的 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&lt;ExampleModel&gt; 来测试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 方法覆盖DbSet 中的Find 方法会更好吗?此外,这种方法只有在它们只有一个主键时才真正有效。如果我的模型有不同类型的复合键怎么办?我想我必须单独处理这些。好的,这是三个问题?

【问题讨论】:

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


    【解决方案1】:

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

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

    class MyRepository
    {
       public MyRepository(IDbSet<MyType> dbSet) 
       {
         this.dbSet = dbSet;
       }
    
       MyType FindEntity(int id)
       {
         return this.dbSet.Find(id);
       }
    }
    

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

    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&lt;MyType&gt; 版本。

    附带说明,如果您发现自己在测试 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);   
      } 
    }
    

    【讨论】:

    • 其实,我上面列出的所有代码都是基于 MSDN 文章(工作单元之外,这是一个通用的表示)。另外,除非我在这里错了,至于依赖DbSet 上的存储库应由 IoC 容器处理。此外,IDbSet 没有很多异步函数,因此在我的 DbContext 中,我将所有 DbSet 都设为虚拟,它们非常可测试。问题与 Find 方法的工作方式有关。如文章中所述,它没有很好地测试。根据我的经验,它在单元测试中总是会返回 null。
    • 也就是说,这是一种有趣的方法。
    • 另外,我并不想测试我的 DbContext。我有一个 TestContext,我正在将假数据加载到 TestContext 中,以便我可以运行我的单元测试。
    • “另外,IDbSet 没有很多异步函数,所以在我的 DbContext 中,我将所有 DbSet 都设为虚拟,而且它们非常可测试”——我个人认为您不应该进行测试反对抽象类,但接口。这就是我建议这种方法的原因。
    • 我听到了,但这是 EF6 msdn.microsoft.com/en-us/data/dn314429.aspx#virtual 的推荐方法。来自文章 - “请注意,上下文上的 DbSet 属性被标记为虚拟。这将允许模拟框架从我们的上下文中派生并使用模拟实现覆盖这些属性。”
    【解决方案2】:

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

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

    1. “身份证”
    2. 'ID'
    3. 'id'
    4. 类名+'id'
    5. 类名+'Id'
    6. 类名 + '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);
      
      }
      

    同样从性能的角度来看,它不会像推荐的方法那么快,但就我的目的而言,它很好。

    【讨论】:

      【解决方案3】:

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

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

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

      3. 我按照Creating the in-memory test doubles 示例创建了 TestDbContext 和所有推荐的类(例如 TestDbAsyncQueryProvider、TestDbAsyncEnumerable、TestDbAsyncEnumerator。)这是代码:

        公共类 TestContext : DbContext, IApplicationDbContext { 公共测试上下文() {

             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);
          }
      }
      
      1. 创建了一个 TestContext 实例并将其传递给我的 Mock。

        var context = new TestContext(); var userStore = new Mock();

        //ExpectedGlobal 包含一个静态变量调用 Expected_User //用于填充原理 // 模拟 HttpRequestContext 时 用户商店 .Setup(m => m.FindByIdAsync(ExpectedGlobal.Expected_User.Id)) .Returns(Task.FromResult(ExpectedGlobal.Expected_User));

        var mockUserManager = new Mock(userStore.Object);

        var mockUnitOfWork = 新模拟(mockUserManager.Object,上下文) { CallBase = false };

      然后我将 mockUnitOfWork 注入控制器,瞧。这个实现似乎很完美。也就是说,根据我在网上阅读的一些提要,它可能会受到一些开发者的审查,但我希望其他一些人觉得这很有用。

      【讨论】:

      • 再一次,您仍然在针对实现而不是接口进行编码。 :\ 如果它有效,它就有效,但这是我对这个解决方案的抱怨
      • @DanPantry - 同样,这来自 MSDN 提供的示例。我听到了您的抱怨,但是当我尝试模拟上下文时,它无法正常工作。我确实忘记提及 dbset 是虚拟的。我已经更新了代码。
      猜你喜欢
      • 2014-11-19
      • 2019-07-07
      • 1970-01-01
      • 2017-12-01
      • 2021-09-19
      • 1970-01-01
      • 2015-12-20
      • 1970-01-01
      • 2023-03-06
      相关资源
      最近更新 更多