[英]How to implement FIND method of EF in Unit Test?
我有一个Web API 2.0项目,我正在进行单元测试。 我的控制器有一个工作单元。 工作单元包含许多用于各种DbSet的存储库。 我在Web API中有一个Unity容器,我在测试项目中使用Moq。 在各种存储库中,我使用Entity Framework的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:使用您自己的测试加倍测试(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);
}
}
}
我想我必须自定义我的代码,以便TEntity
是一种具有Id属性的基类的类型。 这是我的理论,但我不确定这是处理这个问题的最好方法。
所以我有两个问题。 上面列出的方法是否有效? 如果没有,使用SingleOrDefault
方法覆盖DbSet
的Find
方法会有什么更好的方法? 此外,这种方法只有在它们只有一个主键时才能真正起作用。 如果我的模型有不同类型的复合键怎么办? 我认为我必须单独处理这些。 好的,那是三个问题?
为了扩展我之前的评论,我将从我提出的解决方案开始,然后解释原因。
您的问题是:您的存储库依赖于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);
}
}
通过将依赖关系从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);
}
那里 - 一个干净的测试,不暴露任何实现细节或处理具体类的讨厌IDbSet<MyType>
并允许您替换自己的IDbSet<MyType>
版本。
另外,如果您发现自己正在测试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);
}
}
我意识到这是一个老问题,但是在我为单元测试模拟数据时遇到这个问题后,我编写了这个'Find'方法的通用版本,可以在msdn上解释的TestDBSet实现中使用
使用此方法意味着您不必为每个DbSet创建具体类型。 需要注意的一点是,如果您的实体具有以下某种形式的主键,则此实现有效(确保您可以轻松修改以适应其他表单):
classname +'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); }
从性能的角度来看,它不会像推荐的方法一样快,但对于我的目的来说它很好。
因此,基于该示例,我执行了以下操作以便能够对UnitOfWork进行单元测试。
1)必须确保我的UnitOfWork正在实现IApplicationDbContext。 (另外,当我说UnitOfWork时,我的控制器的UnitOfWork是IUnitOfWork类型。)
2)我单独留下了IApplicationDbContext中的所有DbSet。 一旦我注意到IDbSet不包含我在整个代码中使用的RemoveRange和FindAsync,我就选择了这个模式。 此外,使用EF6,DbSet可以设置为虚拟,这在MSDN中推荐,所以这是有道理的。
3)我按照创建内存中测试双打示例来创建TestDbContext和所有推荐的类(例如TestDbAsyncQueryProvider,TestDbAsyncEnumerable,TestDbAsyncEnumerator。)这是代码:
public class TestContext : DbContext, IApplicationDbContext
{
public TestContext()
{
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);
}
}
4)创建了一个TestContext实例并将其传递给我的Mock。
var context = new TestContext();
var userStore = new Mock<IUserStore<ApplicationUser>>();
//ExpectedGlobal contains a static variable call Expected_User
//to be used as to populate the principle
// when mocking the HttpRequestContext
userStore
.Setup(m => m.FindByIdAsync(ExpectedGlobal.Expected_User.Id))
.Returns(Task.FromResult(ExpectedGlobal.Expected_User));
var mockUserManager = new Mock<ApplicationUserManager>(userStore.Object);
var mockUnitOfWork =
new Mock<IUnitOfWork>(mockUserManager.Object, context)
{ CallBase = false };
然后我将mockUnitOfWork注入控制器,瞧。 这种实现似乎是完美的。 也就是说,基于我在线阅读的一些提要,它可能会被一些开发人员仔细检查,但我希望其他人认为这有用。
〜干杯
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.