Unit testing mocked repositories in a C# Unit of Work project?

huangapple go评论64阅读模式
英文:

Unit testing mocked repositories in a C# Unit of Work project?

问题

我们的项目是一个使用CLEAN模式设置的Swashbuckle.AspNetCore项目。
基础设施层采用了Unit of Work模式。

我试图使用MSTest和Moq4创建一个单元测试仓库。现在我的问题是如何正确地单元测试Service?依赖注入对我来说太复杂了。我不明白的是如何在单元测试函数中实例化一个Service对象,该对象具有一个模拟的ApplicationDbContext的Unit of Work对象。

就我所知,GenericRepository<T>,上下文和UnitOfWork没有紧密耦合(如21847306/how-to-mock-repository-unit-of-work中建议的那样)。

我正在处理的代码如下所示:

public interface IGenericRepository&lt;T&gt; where T : class
{
    IQueryable&lt;T&gt; All();
    void Delete(T entity);
    //.. 其他函数
}

public class GenericRepository&lt;T&gt; : IGenericRepository&lt;T&gt; where T : class
{
    protected readonly ApplicationDBContext _context;
    protected readonly IConfiguration _configuration;
    protected DbSet&lt;T&gt; dbSet;

    public GenericRepository(ApplicationDBContext context, IConfiguration configuration)
    {
        _context = context;
        _configuration = configuration;
        this.dbSet = _context.Set&lt;T&gt;();
    }

    public IQueryable&lt;T&gt; All()
    {
        return _context.Set&lt;T&gt;().AsQueryable().AsNoTracking();
    }

    public void Delete(T entity)
    {
        _context.Set&lt;T&gt;().Remove(entity);
    }

    //.. 其他函数
}

public interface ISomeRepository : IGenericRepository&lt;Some&gt;
{
    public Task&lt;bool&gt; AddSomeWithCustomLogicAsync(Some some);
    public Task&lt;bool&gt; DeleteSomeWithCustomLogicAsync(int someId);
}

class SomeRepository : GenericRepository&lt;Some&gt;, ISomeRepository
{
    public SomeRepository(ApplicationDBContext dbContext, IConfiguration configuration) : base(dbContext, configuration)
    {
    }

    public async Task&lt;bool&gt; AddSomeWithCustomLogicAsync(Some some)
    {
        // 添加逻辑..
    }

    public async Task&lt;bool&gt; DeleteSomeWithCustomLogicAsync(int someId)
    {
        // 删除逻辑..
    }
}
public interface IUnitOfWork : IDisposable
{
    public ISomeRepository SomeRepository { get; }
    public IAnotherRepository AnotherRepository { get; }

    int SaveChanges();
}

public class UnitOfWork : IUnitOfWork
{
    private readonly ApplicationDBContext _dBContext;
    private readonly IConfiguration _configuration;
    private readonly ILogger _logger;

    private ISomeRepository _someRepository;
    private IAnotherRepository _anotherRepositoty;

    public UnitOfWork(ApplicationDBContext applicationDBContext, ILoggerFactory loggerFactory, IConfiguration configuration)
    {
        _dBContext = applicationDBContext;
        _configuration = configuration;
        _logger = loggerFactory.CreateLogger("logs");
    }

    public ISomeRepository SomeRepository
    {
        get
        {
            _someRepository ??= new SomeRepository(_dBContext, _configuration);
            return _someRepository;
        }
    }

    public int SaveChanges()
    {
        return _dBContext.SaveChanges();
    }
}
public abstract class GenericService&lt;T&gt; : IGenericService&lt;T&gt; where T : class
{
    public IUnitOfWork _unitOfWork;

    protected readonly IMapper _mapper;
    protected readonly IValidateService _validateService;
    protected readonly ISignalService _signalService;
    protected readonly ILogBoekService _logBoekService;
    protected readonly IHttpContextAccessor _context;
    private readonly IUriService _uriService;

    public GenericService(IUnitOfWork unitOfWork, IMapper mapper, IValidateService validateService, ISignalService signalService,
                          ILogBoekService logBoekService, IHttpContextAccessor context, IUriService uriService)
    {
        _unitOfWork = unitOfWork;
        _mapper = mapper;
        _validateService = validateService;
        _signalService = signalService;
        _logBoekService = logBoekService;
        _context = context;
        _uriService = uriService;
    }
}

public interface ISomeService : IGenericService&lt;Some&gt;
{
    public Task&lt;bool&gt; DoWork();
}

public class SomeService : GenericService&lt;Some&gt;, ISomeService
{
    public SomeService(IUnitOfWork unitOfWork, IMapper mapper, IValidateService validateService,
                       ISignalService signalService, ILogBoekService logBoekService, IHttpContextAccessor context,
                       IUriService uriService)
        : base(unitOfWork, mapper, validateService, signalService, logBoekService, context, uriService)
    {
    }

    // 这是我想要测试的函数
    public async Task&lt;bool&gt; DoWork()
    {
        return await _unitOfWork.SomeRepository.All() == 0;
    }
}
[TestClass]
public class SomeUnitTest
{
    private SomeService _someService;

    public void GenerateService(IQueryable&lt;Some&gt; documenten)
    {
        var mockDbSet = new Mock&lt;DbSet&lt;Some&gt;&gt;();
        mockDbSet.As&lt;IQueryable&lt;Some&gt;&gt;().Setup(x =&gt; x.Provider).Returns(documenten.Provider);
        mockDbSet.As&lt;IQueryable&lt;Some&gt;&gt;().Setup(x =&gt; x.Expression).Returns(documenten.Expression);
        mockDbSet.As&lt;IQueryable&lt;Some&gt;&gt;().Setup(x =&gt; x.ElementType).Returns(documenten.ElementType);
        mockDbSet.As&lt;IQueryable&lt;Some&gt;&gt;().Setup(x =&gt; x.GetEnumerator()).Returns(documenten.GetEnumerator());

        var mockContext = new Mock&lt;ApplicationDBContext&gt;();
        mockContext.Setup(x =&gt; x.Some).Returns(mockDbSet.Object);

        // 这似乎是一个低效的方法,如何改进它?
        var unitOfWork = new UnitOfWork(mockContext.Object, null, null);
        _someService = new SomeService(unitOfWork, null, null, null, null, null, null);
    }

    [TestMethod]
    public async Task GetPaginatedSomeAsyncTest()
    {
        // 准备数据

        var someThings = new List&lt;Some&gt; {
            new Some { Id = 1, Name = "Some 1" },
            new Some { Id = 2, Name = "Some 2" },
            new Some { Id = 3, Name = "Some 3" }
        }.AsQueryable();

        GenerateService(someThings);

        // 测试

        var retrievedDocumenten = await _someService.DoWork();

        Assert.AreEqual(0, retrievedDocumenten.Data.Count);

        return;
    }

    [TestMethod]
    public void GetSomeAsyncTest()
    {
        Assert

<details>
<summary>英文:</summary>

Our project is a Swashbuckle.AspNetCore project setup using the CLEAN pattern.
The infrastructure layer is structured with the Unit of Work pattern.

I&#39;m trying to create a unit test repository using MSTest and Moq4. Now my
question is how do I Unit test the Service properly? The dependency injection is
too complicated for me. What I dont understand is how to instantiate a Service object in
the unit test function with a UnitOfWork object that has a mocked
ApplicationDbContext.

As far as I can tell the GenericRepository&lt;T&gt;, context and UnitOfWork are not tightly coupled
(as recommended in [21847306/how-to-mock-repository-unit-of-work][1]).

The code I&#39;m working with looks like the following:

```csharp
public interface IGenericRepository&lt;T&gt; where T : class
{
	IQueryable&lt;T&gt; All();
	void Delete(T entity);
	//.. Other functions
}

public class GenericRepository&lt;T&gt; : IGenericRepository&lt;T&gt; where T : class
{
	protected readonly ApplicationDBContext _context;
	protected readonly IConfiguration _configuration;
	protected DbSet&lt;T&gt; dbSet;

	public GenericRepository(ApplicationDBContext context, IConfiguration configuration)
	{
		_context = context;
		_configuration = configuration;
		this.dbSet = _context.Set&lt;T&gt;();
	}

	public IQueryable&lt;T&gt; All()
	{
		return _context.Set&lt;T&gt;().AsQueryable().AsNoTracking();
	}

	public void Delete(T entity)
	{
		_context.Set&lt;T&gt;().Remove(entity);
	}

	//.. Other functions
}

public interface ISomeRepository : IGenericRepository&lt;Some&gt;
{
	public Task&lt;bool&gt; AddSomeWithCustomLogicAsync(Some some);
	public Task&lt;bool&gt; DeleteSomeWithCustomLogicAsync(int someId);
}

class SomeRepository : GenericRepository&lt;Some&gt;, ISomeRepository
{
	public SomeRepository(ApplicationDBContext dbContext, IConfiguration configuration) : base(dbContext, configuration)
	{
	}

	public async Task&lt;bool&gt; AddSomeWithCustomLogicAsync(Some some)
	{
		// Add logic..
	}

	public async Task&lt;bool&gt; DeleteSomeWithCustomLogicAsync(int someId)
	{
		// Delete logic..
	}
}
public interface IUnitOfWork : IDisposable
{
	public ISomeRepository SomeRepository { get; }
	public IAnotherRepository AnotherRepository { get; }

	int SaveChanges();
}

public class UnitOfWork : IUnitOfWork
{
	private readonly ApplicationDBContext _dBContext;
	private readonly IConfiguration _configuration;
	private readonly ILogger _logger;

	private ISomeRepository _someRepository;
	private IAnotherRepository _anotherRepositoty;

	public UnitOfWork(ApplicationDBContext applicationDBContext, ILoggerFactory loggerFactory, IConfiguration configuration)
	{
		_dBContext = applicationDBContext;
		_configuration = configuration;
		_logger = loggerFactory.CreateLogger(&quot;logs&quot;);
	}

	public ISomeRepository SomeRepository
	{
		get
		{
			_someRepository ??= new SomeRepository(_dBContext, _configuration);
			return _someRepository;
		}
	}

	public int SaveChanges()
	{
		return _dBContext.SaveChanges();
	}
}
public abstract class GenericService&lt;T&gt; : IGenericService&lt;T&gt; where T : class
{
	public IUnitOfWork _unitOfWork;

	protected readonly IMapper _mapper;
	protected readonly IValidateService _validateService;
	protected readonly ISignalService _signalService;
	protected readonly ILogBoekService _logBoekService;
	protected readonly IHttpContextAccessor _context;
	private readonly IUriService _uriService;

	public GenericService(IUnitOfWork unitOfWork, IMapper mapper, IValidateService validateService, ISignalService signalService,
						  ILogBoekService logBoekService, IHttpContextAccessor context, IUriService uriService)
	{
		_unitOfWork = unitOfWork;
		_mapper = mapper;
		_validateService = validateService;
		_signalService = signalService;
		_logBoekService = logBoekService;
		_context = context;
		_uriService = uriService;
	}
}

public interface ISomeService : IGenericService&lt;Some&gt;
{
	public Task&lt;bool&gt; DoWork();
}

public class SomeService : GenericService&lt;Some&gt;, ISomeService
{
	public SomeService(IUnitOfWork unitOfWork, IMapper mapper, IValidateService validateService,
					   ISignalService signalService, ILogBoekService logBoekService, IHttpContextAccessor context,
					   IUriService uriService)
		: base(unitOfWork, mapper, validateService, signalService, logBoekService, context, uriService)
	{
	}

	// This is the function I want to test
	public async Task&lt;bool&gt; DoWork()
	{
		return await _unitOfWork.SomeRepository.All() == 0;
	}
}
[TestClass]
public class SomeUnitTest
{
	private SomeService _someService;

	public void GenerateService(IQueryable&lt;Some&gt; documenten)
	{
		var mockDbSet = new Mock&lt;DbSet&lt;Some&gt;&gt;();
		mockDbSet.As&lt;IQueryable&lt;Some&gt;&gt;().Setup(x =&gt; x.Provider).Returns(documenten.Provider);
		mockDbSet.As&lt;IQueryable&lt;Some&gt;&gt;().Setup(x =&gt; x.Expression).Returns(documenten.Expression);
		mockDbSet.As&lt;IQueryable&lt;Some&gt;&gt;().Setup(x =&gt; x.ElementType).Returns(documenten.ElementType);
		mockDbSet.As&lt;IQueryable&lt;Some&gt;&gt;().Setup(x =&gt; x.GetEnumerator()).Returns(documenten.GetEnumerator());

		var mockContext = new Mock&lt;ApplicationDBContext&gt;();
		mockContext.Setup(x =&gt; x.Some).Returns(mockDbSet.Object);

		// This seems like an inefficient way of doing it, how can it be improved?
		var unitOfWork = new UnitOfWork(mockContext.Object, null, null);
		_someService = new SomeService(unitOfWork, null, null, null, null, null, null);
	}

	[TestMethod]
	public async Task GetPaginatedSomeAsyncTest()
	{
		// Prepare data

		var someThings = new List&lt;Some&gt; {
			new Some { Id = 1, Name = &quot;Some 1&quot; },
			new Some { Id = 2, Name = &quot;Some 2&quot; },
			new Some { Id = 3, Name = &quot;Some 3&quot; }
		}.AsQueryable();

		GenerateService(someThings);

		// Test

		var retrievedDocumenten = await _someService.DoWork();

		Assert.AreEqual(0, retrievedDocumenten.Data.Count);

		return;
	}

	[TestMethod]
	public void GetSomeAsyncTest()
	{
		Assert.Fail();
	}
}

I did not succeed at creating a mocked UnitOfWork object properly, I dont know how to reproduce the dependency injection that occurs automatically in the unit test.

答案1

得分: 1

一个单元测试应该与其所有外部依赖分离,否则你就会进入集成测试的领域。由于你正试图对服务进行单元测试,你只需要模拟这些接口——这意味着你根本不需要模拟ApplicationDBContext。这就是分离的美妙之处。

var mockRepository = new Mock&lt;ISomeRepository&gt;();
var mockUnitOfWork = new Mock&lt;IUnitOfWork&gt;();
var service = new SomeService(mockUnitOfWork.Object, Mock.Of&lt;IMapper&gt;(), Mock.Of&lt;IValidateService&gt;(), Mock.Of&lt;ISignalService&gt;(), Mock.Of&lt;ILogBoekService&gt;(), Mock.Of&lt;IHttpContextAccessor&gt;(), Mock.Of&lt;IUriService&gt;());

// 设置模拟的 UOW 返回模拟的仓库
mockUnitOfWork
    .Setup(uow =&gt; uow.SomeRepository)
    .Returns(mockRepository.Object)

// 在这里模拟任何仓库调用
mockRepository.Setup(...).Returns(...)

// 调用你的服务
var retrievedDocumenten = service.DoWork();

// 断言你的结果
Assert.AreEqual(0, retrievedDocumenten.Data.Count);

// 可选地断言仓库调用
mockRepository.Verify(...)
英文:

A Unit Test should be separated from all of it's external dependencies, otherwise you enter integration test territory. Since you are trying to unit test your Service, you only need to mock those interfaces - meaning you don't need to mock the ApplicationDBContext at all. This is the beauty of separation.

var mockRepository = new Mock&lt;ISomeRepository&gt;();
var mockUnitOfWork = new Mock&lt;IUnitOfWork&gt;();
var service = new SomeService(mockUnitOfWork.Object, Mock.Of&lt;IMapper&gt;(), Mock.Of&lt;IValidateService&gt;(), Mock.Of&lt;ISignalService&gt;(), Mock.Of&lt;ILogBoekService&gt;(), Mock.Of&lt;IHttpContextAccessor&gt;(), Mock.Of&lt;IUriService&gt;());

// Setup the mock UOW to return the mock repository
mockUnitOfWork
    .Setup(uow =&gt; uow.SomeRepository)
    .Returns(mockRepository.Object)

// Mock any repository calls here
mockRepository.Setup(...).Returns(...)

// Call your service
var retrievedDocumenten = service.DoWork();

// Assert your result
Assert.AreEqual(0, retrievedDocumenten.Data.Count);

// Optionally assert repository calls
mockRepository.Verify(...)

huangapple
  • 本文由 发表于 2023年2月24日 00:07:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/75547368.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定