我有一个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<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属性的基类的类型。这是我的理论,但我不确定这是处理这个问题的最好方法。
所以我有两个问题。上面列出的方法是否有效?如果不是,使用Find
方法覆盖DbSet
中SingleOrDefault
方法的更好方法是什么?此外,这种方法只有在它们只有一个主键时才能真正起作用。如果我的模型有不同类型的复合键怎么办?我认为我必须单独处理这些。好的,那是三个问题?
答案 0 :(得分:5)
为了扩展我之前的评论,我将从我提出的解决方案开始,然后解释原因。
您的问题是:您的存储库依赖于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>
版本。
另一方面,如果您发现自己正在测试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);
}
}
答案 1 :(得分:3)
我意识到这是一个老问题,但是在我为单元测试模拟数据时遇到这个问题后,我写了这个&#39; Find&#39;可以在msdn
中解释的TestDBSet实现中使用的方法使用此方法意味着您无需为每个DbSet创建具体类型。需要注意的一点是,如果您的实体具有以下某种形式的主键,则此实现有效(确保您可以轻松修改以适应其他形式):
classname +&#39; ID&#39;
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);
}
同样从性能的角度来看,它不会像推荐的方法一样快,但对于我的目的来说还不错。
答案 2 :(得分:0)
基于这个例子,我做了以下工作,以便能够对UnitOfWork进行单元测试。
1)必须确保我的UnitOfWork正在实现IApplicationDbContext。 (另外,当我说UnitOfWork时,我的控制器的UnitOfWork是IUnitOfWork类型。)
2)我单独留下了IApplicationDbContext中的所有DbSet。一旦我注意到IDbSet不包含我在整个代码中使用的RemoveRange和FindAsync,我就选择了这个模式。此外,使用EF6,DbSet可以设置为虚拟,这在MSDN中是推荐的,所以这是有道理的。
3)我按照Creating the in-memory test doubles 示例创建了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实例并将其传递给我的模拟。
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注入控制器,瞧。这种实现似乎是完美的。也就是说,基于我在线阅读的一些提要,它可能会被一些开发人员仔细审查,但我希望其他人认为这有用。
〜干杯