【问题标题】:Unit testing with EF4 "Code First" and Repository使用 EF4“代码优先”和存储库进行单元测试
【发布时间】:2011-03-28 15:23:15
【问题描述】:

我正在尝试对一个非常简单的 ASP.NET MVC 测试应用程序进行单元测试,这是我在最新的 EF4 CTP 中使用 Code First 方法构建的。我对单元测试/模拟等不是很有经验。

这是我的存储库类:

public class WeightTrackerRepository
{
    public WeightTrackerRepository()
    {
        _context = new WeightTrackerContext();
    }

    public WeightTrackerRepository(IWeightTrackerContext context)
    {
        _context = context;
    }

    IWeightTrackerContext _context;

    public List<WeightEntry> GetAllWeightEntries()
    {
        return _context.WeightEntries.ToList();
    }

    public WeightEntry AddWeightEntry(WeightEntry entry)
    {
        _context.WeightEntries.Add(entry);
        _context.SaveChanges();
        return entry;
    }
}

这是 IWeightTrackerContext

public interface IWeightTrackerContext
{
    DbSet<WeightEntry> WeightEntries { get; set; }
    int SaveChanges();
}

...及其实现,WeightTrackerContext

public class WeightTrackerContext : DbContext, IWeightTrackerContext
{
    public DbSet<WeightEntry> WeightEntries { get; set; }
}

在我的测试中,我有以下几点:

[TestMethod]
public void Get_All_Weight_Entries_Returns_All_Weight_Entries()
{
    // Arrange
    WeightTrackerRepository repos = new WeightTrackerRepository(new MockWeightTrackerContext());

    // Act
    List<WeightEntry> entries = repos.GetAllWeightEntries();

    // Assert
    Assert.AreEqual(5, entries.Count);
}

还有我的 MockWeightTrackerContext:

class MockWeightTrackerContext : IWeightTrackerContext
{
    public MockWeightTrackerContext()
    {
        WeightEntries = new DbSet<WeightEntry>();
        WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("01/06/2010"), Id = 1, WeightInGrams = 11200 });
        WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("08/06/2010"), Id = 2, WeightInGrams = 11150 });
        WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("15/06/2010"), Id = 3, WeightInGrams = 11120 });
        WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("22/06/2010"), Id = 4, WeightInGrams = 11100 });
        WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("29/06/2010"), Id = 5, WeightInGrams = 11080 });
    }

    public DbSet<WeightEntry> WeightEntries { get;set; }

    public int SaveChanges()
    {
        throw new NotImplementedException();
    }
}

当我尝试构建一些测试数据时出现我的问题,因为我无法创建 DbSet&lt;&gt;,因为它没有构造函数。我觉得我用我的整个方法试图模仿我的上下文是在叫错树。任何建议都欢迎这个完整的单元测试新手。

【问题讨论】:

    标签: c# asp.net-mvc unit-testing mocking entity-framework-4


    【解决方案1】:

    您通过 Context 上的工厂方法 Set() 创建了一个 DbSet,但您不希望在单元测试中对 EF 有任何依赖。因此,您需要做的是使用 IDbSet 接口实现 DbSet 存根,或者使用 Moq 或 RhinoMock 等 Mocking 框架之一实现存根。假设您编写了自己的 Stub,您只需将 WeightEntry 对象添加到内部哈希集。

    如果您搜索 ObjectSet 和 IObjectSet,您可能会更幸运地了解单元测试 EF。这些是代码优先 CTP 发布之前的 DbSet 的对应物,并且从单元测试的角度写了很多关于它们的文章。

    这是 MSDN 上的 excellent article,它讨论了 EF 代码的可测试性。它使用 IObjectSet 但我认为它仍然相关。

    作为对 David 评论的回应,我在下面添加了这个附录,因为它不适合 -cmets。不确定这是否是长评论回复的最佳做法?

    您应该更改 IWeightTrackerContext 接口以从 WeightEntries 属性返回 IDbSet 而不是 DbSet 具体类型。然后,您可以使用模拟框架(推荐)或您自己的自定义存根创建 MockContext。这将从 WeightEntries 属性返回一个 StubDbSet。

    现在,您还将拥有依赖于 IWeightTrackerContext 的代码(即自定义存储库),在生产中您将在实现 IWeightTrackerContext 的实体框架 WeightTrackerContext 中传递这些代码。这往往是通过使用 IoC 框架(如 Unity)的构造函数注入来完成的。为了测试依赖于 EF 的存储库代码,您将传入您的 MockContext 实现,以便被测代码认为它正在与“真实”的 EF 和数据库对话,并且行为(希望)符合预期。由于您已经消除了对可变外部数据库系统和 EF 的依赖,因此您可以在单元测试中可靠地验证存储库调用。

    模拟框架的很大一部分是提供验证对 Mock 对象的调用以测试行为的能力。在上面的示例中,您的测试实际上只是测试 DbSet Add 功能,这不应该是您关心的问题,因为 MS 将对此进行单元测试。您想知道的是,如果合适的话,对 Add on DbSet 的调用是在您自己的存储库代码中进行的,这就是 Mock 框架的用武之地。

    对不起,我知道这需要消化很多东西,但是如果您阅读了那篇文章,就会变得更清楚,因为 Scott Allen 比我更擅长解释这些东西 :)

    【讨论】:

    • 感谢您的链接,这看起来是一篇很棒的文章。关于实现 DbSet 的存根,您是说我的 MockWeightTrackerContext 会返回,比如说,MyDbSetStub 而不是 DbSet?但是那个类不会实现接口。对不起(可能是愚蠢的)问题。 ;)
    • 非常感谢您花时间解释这一点。这和您提供的链接有很大帮助。如果我能再次投票给你的答案,我会的。 :D
    • 谢谢,实现 InMemoryObjectSet 的 MSDN 文章正是我让 IContext 正常工作所需要的。
    • 您将需要一个 IDbSet 的“假”实现,它基本上只是一个内存中的集合。
    【解决方案2】:

    根据 Daz 所说的(正如我在他的 cmets 中提到的),您将需要对 IDbSet 进行“假”实现。 CTP 4 的示例可以在 here 找到,但要使其正常工作,您必须自定义 Find 方法并为之前的几个 void ed 方法添加返回值,例如 Add。

    以下是我自己为 CTP 5 精心设计的示例:

    public class InMemoryDbSet<T> : IDbSet<T> where T : class
    {
        readonly HashSet<T> _data;
        readonly IQueryable _query;
    
        public InMemoryDbSet()
        {
            _data = new HashSet<T>();
            _query = _data.AsQueryable();
        }
    
        public T Add(T entity)
        {
            _data.Add(entity);
            return entity;
        }
    
        public T Attach(T entity)
        {
            _data.Add(entity);
            return entity;
        }
    
        public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
        {
            throw new NotImplementedException();
        }
    
        public T Create()
        {
            return Activator.CreateInstance<T>();
        }
    
        public virtual T Find(params object[] keyValues)
        {
            throw new NotImplementedException("Derive from FakeDbSet and override Find");
        }
    
        public System.Collections.ObjectModel.ObservableCollection<T> Local
        {
            get { return new System.Collections.ObjectModel.ObservableCollection<T>(_data); }
        }
    
        public T Remove(T entity)
        {
            _data.Remove(entity);
            return entity;
        }
    
        public IEnumerator<T> GetEnumerator()
        {
            return _data.GetEnumerator();
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return _data.GetEnumerator();
        }
    
        public Type ElementType
        {
            get { return _query.ElementType; }
        }
    
        public Expression Expression
        {
            get { return _query.Expression; }
        }
    
        public IQueryProvider Provider
        {
            get { return _query.Provider; }
        }
    }
    

    【讨论】:

    • 有用的类。感谢分享!
    • 如果您在构造函数中为 Find 提供委托,它可能会变得更加有用: private readonly Func, T> _finder;公共 InMemoryDbSet(Func, T> finder) { _finder = finder; _data = new HashSet(); _query = _data.AsQueryable(); } 和 public virtual T Find(params object[] keyValues) { return _finder(keyValues, _data); }
    【解决方案3】:

    你可能会遇到错误

     AutoFixture was unable to create an instance from System.Data.Entity.DbSet`1[...], most likely because it has no public constructor, is an abstract or non-public type.
    

    DbSet 有一个受保护的构造函数。

    通常用于支持可测试性的方法是创建您自己的 Db Set 实现

    public class MyDbSet<T> : IDbSet<T> where T : class
    {
    

    把它当作

    public class MyContext : DbContext
    {
        public virtual MyDbSet<Price> Prices { get; set; }
    }
    

    如果您查看下面的 SO question, 2ns answer,您应该能够找到自定义 DbSet 的完整实现。

    Unit testing with EF4 "Code First" and Repository

    然后

     var priceService = fixture.Create<PriceService>();
    

    不应抛出异常。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-02-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多