如何在C#中以CQRS模式对命令处理程序进行单元测试

问题描述

我正在C#中学习和练习CQRS和单元测试。我需要知道如何在CQRS中对命令处理程序进行单元测试。

这是我的C:\>dir C:\etc\*.exe | find /i "timeout" 12/02/1999 03:54 PM 62,464 TIMEOUT.EXE C:\>C:\etc\timeout /? TIMEOUT - This utility is similar to the DOS PAUSE command. However,it accepts a timeout parameter to specify a length of wait (in seconds) at which time it will continue without a key press. Written by Eric brown,Business System Division. copyright (C) Microsoft Corporation,1992-1995. Usage - TIMEOUT <###> where <###> is a decimal number of seconds between -1 and 100000. The value -1 means to wait indefinitely for a key hit.

CreateAuthorCommand

这是我的单元测试:

public sealed class CreateAuthorCommand : ICommand<Author>
{
    public CreateAuthorCommand(string firstName,string lastName,DateTimeOffset dateOfBirth,string mainCategory,IEnumerable<CreateBookDto> books)
    {
        FirstName = firstName;
        LastName = lastName;
        DateOfBirth = dateOfBirth;
        MainCategory = mainCategory;
        Books = books;
    }

    public string FirstName { get; }
    public string LastName { get; }
    public DateTimeOffset DateOfBirth { get; private set; }
    public string MainCategory { get; }
    public IEnumerable<CreateBookDto> Books { get; }

    [AuditLog]
    [DatabaseRetry]
    internal sealed class AddCommandHandler : ICommandHandler<CreateAuthorCommand,Author>
    {
        private readonly IUnitOfWork _unitOfWork;

        public AddCommandHandler(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
        }

        public async Task<Result<Author>> Handle(CreateAuthorCommand command)
        {
            var nameResult = Name.Create(command.FirstName,command.LastName);
            var birthDateResult = BirthDate.Create(command.DateOfBirth);
            var mainCategoryResult = Entities.Authors.MainCategory.Create(command.MainCategory);

            var authorResult = Result.Combine(nameResult,birthDateResult,mainCategoryResult)
                                .Map(() => new Author(nameResult.Value,birthDateResult.Value,null,mainCategoryResult.Value));
                
            if (authorResult.IsFailure)
                return Result.Failure<Author>(authorResult.Error);

            await _unitOfWork.AuthorRepository.AddAsync(authorResult.Value);

            await _unitOfWork.SaveChangesAsync();

            return Result.Success(authorResult.Value);
        }
    }
}

当我调试上面的测试时,

  1. 我无法进入处理程序方法
  2. 如何将public class CreateAuthorCommandTests { [Fact] public void Create_Author_Should_Call_Add_Method_Once() { var fixture = new Fixture(); var command = fixture.Create<CreateAuthorCommand>(); var mockUnitOfWork = new Mock<IUnitOfWork>(); var mockHandler = new Mock<ICommandHandler<CreateAuthorCommand,Author>>(); var result = mockHandler.Object.Handle(command); mockUnitOfWork.Verify(x => x.AuthorRepository.AddAsync(It.IsAny<Author>()),Times.Once); } } 作为构造函数参数传递给mockUnitOfWork

如果我可以通过AddCommandHandler,则可以验证mockUnitOfWork内部的AddAsync呼叫。请协助我做错事。

解决方法

您正在测试处理程序,所以代替此

var result = mockHandler.Object.Handle(command);

...创建一个AddCommandHandler的实际实例并注入其所需的依赖项,即

var mockUnitOfWork = new Mock<IUnitOfWork>();
var handler = new AddCommandHandler(mockUnitOfWork.Object);

调用 Handle 时,您现在将进入实现。

如果要保留“内部访问”修饰符,则可以使用InternalsVisibleTo属性授予对测试项目的访问权限,例如在定义处理程序的项目中执行此操作,

[assembly: InternalsVisibleTo(“UnitTests”)]

https://anthonygiretti.com/2018/06/27/how-to-unit-test-internal-classes-in-net-core-applications/