问题描述
StackOverflow 上有很多与此主题类似的文章,但我没有找到任何专门针对我的情况的文章。
我正在尝试使用 Moq 和 NUnit 对实现了工作单元模式的通用存储库进行单元测试。
这是通用存储库的代码:
public class GenericRepository<TContext,TEntity> : IGenericRepository<TEntity>
where TEntity : class
where TContext : DbContext
{
protected readonly TContext Context;
public GenericRepository(TContext context)
{
Context = context;
}
public async Task<TEntity> Create(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException();
}
await Task.Run(() => Context.Set<TEntity>().Add(entity)).ConfigureAwait(false);
await Context.SaveChangesAsync().ConfigureAwait(false);
return entity;
}
public async Task<IEnumerable<TEntity>> Createrange(IEnumerable<TEntity> entities)
{
if (entities == null)
{
throw new ArgumentNullException();
}
await Task.Run(() => Context.Set<TEntity>().AddRange(entities)).ConfigureAwait(false);
await Context.SaveChangesAsync().ConfigureAwait(false);
return entities;
}
public async Task<TEntity> Retrieve(object entityId)
{
return await Context.Set<TEntity>().FindAsync(entityId).ConfigureAwait(false);
}
public async Task<IEnumerable<TEntity>> Retrieve(Expression<Func<TEntity,bool>> predicate)
{
return await Task.Run(() => Context.Set<TEntity>().Where(predicate)).ConfigureAwait(false);
}
public async Task<TEntity> Update(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException();
}
await Task.Run(() =>
{
Context.Set<TEntity>().AddOrUpdate(entity);
}).ConfigureAwait(false);
await Context.SaveChangesAsync().ConfigureAwait(false);
return entity;
}
public async Task<IEnumerable<TEntity>> Updaterange(IEnumerable<TEntity> entities)
{
var updaterange = entities as TEntity[] ?? entities.ToArray();
if (updaterange.Any(entity => entity == null))
{
throw new ArgumentNullException();
}
foreach (var entity in updaterange)
{
await Task.Run(() =>
{
Context.Set<TEntity>().AddOrUpdate(entity);
}).ConfigureAwait(false);
}
await Context.SaveChangesAsync().ConfigureAwait(false);
return updaterange;
}
public async Task<TEntity> SafeDelete(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException();
}
var propertySet = TrySetProperty(entity,"Is_deleted",true);
if (!propertySet)
{
throw new InvalidOperationException();
}
await Task.Run(() =>
{
Context.Set<TEntity>().AddOrUpdate(entity);
}).ConfigureAwait(false);
await Context.SaveChangesAsync().ConfigureAwait(false);
return entity;
}
public async Task<IEnumerable<TEntity>> SafeDeleterange(IEnumerable<TEntity> entities)
{
var safeDeleterange = entities as TEntity[] ?? entities.ToArray();
if (safeDeleterange.Any(entity => entity == null))
{
throw new ArgumentNullException();
}
var propertySet = false;
foreach (var entity in safeDeleterange)
{
propertySet = TrySetProperty(entity,true);
}
if (!propertySet)
{
throw new InvalidOperationException();
}
foreach (var entity in safeDeleterange)
{
await Task.Run(() =>
{
Context.Set<TEntity>().AddOrUpdate(entity);
}).ConfigureAwait(false);
}
await Context.SaveChangesAsync().ConfigureAwait(false);
return safeDeleterange;
}
public async Task<TEntity> Delete(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException();
}
await Task.Run(() => Context.Set<TEntity>().Remove(entity));
await Context.SaveChangesAsync().ConfigureAwait(false);
return entity;
}
public async Task<IEnumerable<TEntity>> Deleterange(IEnumerable<TEntity> entities)
{
if (entities == null)
{
throw new ArgumentNullException();
}
await Task.Run(() => Context.Set<TEntity>().RemoveRange(entities));
await Context.SaveChangesAsync().ConfigureAwait(false);
return entities;
}
private static bool TrySetProperty(object obj,string property,object value)
{
var prop = obj.GetType().GetProperty(property,BindingFlags.Public | BindingFlags.Instance);
if (prop == null || !prop.CanWrite)
{
return false;
}
prop.SetValue(obj,value,null);
return true;
}
}
还有一个单元测试类:
[TestFixture]
public class GenericRepositoryUnitTests
{
private Mock<IDomainRepository> _repo;
private Mock<IUnitOfWork> _unitOfWork;
private Mock<IUnitOfWorkFactory<DmsDbContext>> _unitOfWorkFactory;
private Mock<DmsDbContext> _mockContext;
private Mock<DbSet<Domain>> _mockSet;
private List<Domain> _domains;
private User _user;
[SetUp]
public void SetUp()
{
var userId = Guid.NewGuid();
_user = new User
{
UsersId = userId,Username = "Test",Password = "Test",First_name = "Test",Last_name = "Test",Img = "Test",Permissions = "Test",Is_deleted = false
};
_domains = new List<Domain>
{
new Domain
{
DomainId = Guid.NewGuid(),Name = "Test",Url = "Test",Is_deleted = false,Users = new List<User>
{
_user,new User
{
UsersId = userId,Is_deleted = false
},new User
{
UsersId = Guid.NewGuid(),Is_deleted = false
}
}
},new Domain
{
DomainId = Guid.NewGuid(),Is_deleted = false
}
}
}
};
_mockSet = new Mock<DbSet<Domain>>();
_mockSet.Setup(m => m.Add(It.IsAny<Domain>())).Returns(new Domain());
_mockContext = new Mock<DmsDbContext>();
_mockContext.Setup(m => m.Set<Domain>()).Returns(_mockSet.Object);
_repo = new Mock<IDomainRepository>{CallBase = true};
_repo.Setup(r => r.Context).Returns(_mockContext.Object);
_repo.Setup(r => r.Create(It.IsAny<Domain>())).Returns(Task.Fromresult(new Domain()));
_repo.Setup(r => r.Createrange(It.IsAny<IEnumerable<Domain>>()))
.Returns(Task.Fromresult(new List<Domain>().AsEnumerable()));
_repo.Setup(r => r.Retrieve(It.IsAny<Guid>())).Returns(Task.Fromresult(_domains.AsEnumerable().First()));
_repo.Setup(r => r.Retrieve(pre => pre.Is_deleted == false))
.Returns(Task.Fromresult(_domains.AsEnumerable()));
_repo.Setup(r => r.Update(It.IsAny<Domain>())).Returns(Task.Fromresult(new Domain()));
_repo.Setup(r => r.Delete(It.IsAny<Domain>())).Returns(Task.Fromresult(new Domain()));
_repo.Setup(r => r.Deleterange(It.IsAny<IEnumerable<Domain>>()))
.Returns(Task.Fromresult(new List<Domain>().AsEnumerable()));
_unitOfWork = new Mock<IUnitOfWork>();
_unitOfWork.Setup(u => u.Domains).Returns(_repo.Object);
_unitOfWorkFactory = new Mock<IUnitOfWorkFactory<DmsDbContext>>();
_unitOfWorkFactory.Setup(u => u.Create(It.IsAny<DmsDbContext>())).Returns(_unitOfWork.Object);
}
[Test]
public async Task Create_NewDomainObject_AddToDatabase()
{
// Arrange
var domain = new Domain
{
DomainId = Guid.NewGuid(),Users = new List<User> { _user }
};
// Act
using var unitOfWork = _unitOfWorkFactory.Object.Create(_mockContext.Object);
await unitOfWork.Domains.Create(domain);
// Assert
_repo.Verify(m => m.Context.Set<Domain>().Add(It.IsAny<Domain>()),Times.Once());
}
[Test]
public void Retrieve_TestDomainObjectById_ReturnsValue()
{
//Arrange
var domainId = Guid.NewGuid();
using var unitOfWork = _unitOfWorkFactory.Object.Create(_mockContext.Object);
// Act
var domain = unitOfWork.Domains.Retrieve(domainId);
// Assert
Assert.NotNull(domain);
}
我无法进行这些测试,也不知道出了什么问题。
谁能解释一下,如何使用上述一组工具正确测试通用存储库?
提前致谢:)
解决方法
扩展海登的评论:您正在测试错误的东西。使用存储库模式(尽管我建议远离通用存储库模式)是将业务逻辑与其对域的依赖隔离开来,以便您可以隔离地对业务逻辑进行单元测试。
举一个简单的例子:如果我有一个控制器或服务,它有一个方法来执行更新,从域中检索一组实体,执行一些验证,应用一些更改,并保存这些实体:
如果我直接使用 DbContext:
public void SomeAction(ViewModel viewModel)
{
using (var context = new AppDbContext())
{
var someEntity = context.Entities.Include(x=> x.Children).Include(x=> x.Category).Single(x => x.Id == viewModel.Id);
if(!x.Category.IsLocked)
{
// copy values from view model to entity...
context.SaveChanges();
}
}
}
这里的问题是测试 SomeAction 我可以定义一个 ViewModel,但是让它实例化一个 DbContext 意味着我对数据源有一个硬依赖,该数据源将在该测试中以可预测的方式运行。我可能希望测试涵盖以下情况的行为:未找到实体、实体具有锁定的类别、实体具有未锁定的类别等。
因此我们引入 Repository 模式来抽象存储库并包含工作单元。该方法更多地朝着以下方向变化:
public void SomeAction(ViewModel viewModel)
{
using (var unitOfWork = UnitOfWorkFactory.Create())
{
var someEntity = ActionRepository.GetEntityById(viewModel.Id)
.Include(x=> x.Children).Include(x=> x.Category)
.Single();
if(!x.Category.IsLocked)
{
// copy values from view model to entity...
unitOfWork.SaveChanges();
}
}
}
UnitOfWorkFactory 和 ActionRepository 是我的控制器的注入依赖项。 ActionRepository 公开了我需要与域交互的方法。在我的示例中,我使用返回 IQueryable<TEntity>
的方法,因为它可以容纳非常薄的抽象来替代。我不测试存储库或工作单元的作用,而是模拟工作单元和存储库,以便我可以编写测试来断言我的控制器操作的作用。我的测试模拟可以模拟返回没有匹配的实体,或具有锁定类别的实体,或针对特定测试场景的完全有效的实体。他们甚至可以断言工作单元上的 SaveChanges 是否应该并且确实被调用。尽管可以说这是深入测试您的方法的特定实现,这可能会使您的测试更加脆弱。最终,您的目标是为特定场景的特定输出编写测试,其中模拟使您能够轻松设置这些场景。