使用EF4“Code First”和存储库进行单元测试

时间:2010-08-11 21:32:01

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

我试图处理Unit测试一个非常简单的ASP.NET MVC测试应用程序,我使用最新的EF4 CTP中的Code First方法构建。我对单元测试/模拟等不太熟悉。

这是我的Repository类:

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<>,因为它没有构造函数。我感觉自己正在用我的整个方法咆哮错误的树,试图模仿我的背景。任何建议都非常欢迎这个完整的单元测试新手。

3 个答案:

答案 0 :(得分:48)

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

如果您搜索ObjectSet和IObjectSet,您可能会有更多关于单元测试EF的运气。在代码首次发布CTP之前,这些是DbSet的对应部分,并且从单元测试的角度来看,它们还有更多关于它们的文章。

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

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

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

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

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

很抱歉,我知道这有很多要消化的内容,但如果你仔细阅读那篇文章,很多内容会变得更加清晰,因为斯科特艾伦在解释这些内容方面比我更好:)

答案 1 :(得分:41)

在Daz所说的基础上(正如我在他的评论中提到的那样),你将需要制作一个IDbSet的“假”实现。可以找到CTP 4的示例here但要使其工作,您必须自定义Find方法并为之前的一些无效方法添加返回值,例如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; }
    }
}

答案 2 :(得分:0)

您可能收到错误

 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问题,2ns回答,你应该能够找到自定义DbSet的完整实现。

Unit testing with EF4 "Code First" and Repository

然后

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

不应该抛出异常。