使用通用存储库、UnitOfWork、NUnit 和 Moq 进行单元测试

问题描述

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 是否应该并且确实被调用。尽管可以说这是深入测试您的方法的特定实现,这可能会使您的测试更加脆弱。最终,您的目标是为特定场景的特定输出编写测试,其中模拟使您能够轻松设置这些场景。