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