【问题标题】:How can you verify a method with a specific NSpecification parameter in C# unit tests?如何在 C# 单元测试中验证具有特定 NSpecification 参数的方法?
【发布时间】:2019-10-15 11:24:32
【问题描述】:

我目前正在为 CommandHandler 编写单元测试。 我使用 Moq 4.12.0 和 xUnit 2.4.1 进行测试。 我想验证是否使用某个NSpecification 调用了一个方法。

我是单元测试领域的新手。

这是 CommandHandler:

public class DeleteAlarmCodesCommandHandler : IRequestHandler<DeleteAlarmCodesCommand, CommandResult<IEnumerable<AlarmCode>>>
    {
        private readonly Domain.Model.IAlarmCodeRepository _alarmCodeRepository;

        public DeleteAlarmCodesCommandHandler(
            Domain.Model.IAlarmCodeRepository alarmCodeRepository)
        {
            _alarmCodeRepository = alarmCodeRepository;
        }

        public async Task<CommandResult<IEnumerable<AlarmCode>>> Handle(DeleteAlarmCodesCommand request, CancellationToken cancellationToken)
        {            
            ASpec<Domain.Model.AlarmCode> spec = Spec<Domain.Model.AlarmCode>.Any;

            if (request.AlarmId != null)
            {
                spec &= Domain.Model.AlarmCodeSpecifications.ForAlarmId(request.AlarmId);
            }

            if (request.LanguageISO != null)
            {
                spec &= Domain.Model.AlarmCodeSpecifications.ForLanguageISO(request.LanguageISO);
            }

            try
            {
                var alarmCodes = await _alarmCodeRepository.DeleteAsync(spec);
                await _alarmCodeRepository.SaveAsync();

                return new CommandResult<IEnumerable<AlarmCode>>(alarmCodes.Select(x => x.ToViewModel()));
            }
            catch (Domain.ApiDomainException ex)
            {
                return new CommandResult<IEnumerable<AlarmCode>>(ApiErrors.FromException(ex));
            }
        }
    }

这是我的单元测试,用于检查是否使用规范调用了 DeleteAsync()。

[Fact]
public async Task Should_DeleteWithAlarmIdOne_WhenCalledWitParameterAlarmIdOne()
{
    // Arrange
    var repo = new Mock<IAlarmCodeRepository>();
    var command = new DeleteAlarmCodesCommand() { AlarmId = 1 };

    var commandHandler = new DeleteAlarmCodesCommandHandler(repo.Object);

    // Act
    var result = await commandHandler.Handle(command, It.IsAny<CancellationToken>());

    var spec = Spec<AlarmCode>.Any & AlarmCodeSpecifications.ForAlarmId(command.AlarmId);

   // Assert            
   repo.Verify(x => x.DeleteAsync(spec), Times.Once);
   repo.Verify(x => x.SaveAsync(), Times.Once);
}

问题在于对两个对象的引用是不同的,因为它们是在需要时创建的。所以 Moq 将它们视为完全不同的对象。 因为当我运行测试时,我在结果窗格中得到以下结果。

   Duration: 182 ms

  Message: 
    Moq.MockException : 
    Expected invocation on the mock once, but was 0 times: x => x.DeleteAsync(x => (True AndAlso (Convert(Convert(x.AlarmId, Int32), Nullable`1) == Convert(value(Services.Domain.Model.AlarmCodeSpecifications+<>c__DisplayClass0_0).alarmId, Nullable`1))))

    Performed invocations:

       Mock<IAlarmCodeRepository:1> (x):

          IAlarmCodeRepository.DeleteAsync(x => (True AndAlso (Convert(Convert(x.AlarmId, Int32), Nullable`1) == Convert(value(Services.Domain.Model.AlarmCodeSpecifications+<>c__DisplayClass0_0).alarmId, Nullable`1))))
          IRepository`1.SaveAsync()

  Stack Trace: 
    Mock.Verify(Mock mock, LambdaExpression expression, Times times, String failMessage)
    Mock`1.Verify[TResult](Expression`1 expression, Func`1 times)
    DeleteAlarmCodesCommandHandler_Handle.Should_DeleteWithAlarmIdOne_WhenCalledWitParameterAlarmIdOne() line 46
    --- End of stack trace from previous location where exception was thrown ---

Moq 是否有办法验证具有复杂对象的调用,例如规范。

或者我怎样才能更好地测试这个?

更新

这是 IAlarmCodeRepository 的定义。

 public interface IAlarmCodeRepository : IRepository<AlarmCode>
    {
        IUnitOfWork UnitOfWork { get; }

        Task AddAsync(AlarmCode entity);

        Task<AlarmCode> GetOneAsync(int id);

        Task<AlarmCode> GetOneAsync(ASpec<AlarmCode> spec);

        Task<IEnumerable<AlarmCode>> FindAsync(ASpec<AlarmCode> spec);

        Task<bool> Exists(ASpec<AlarmCode> spec);

        Task<IEnumerable<AlarmCode>> DeleteAsync(ASpec<AlarmCode> spec);

        Task<AlarmCode> DeleteOne(int id);

        Task<IEnumerable<short>> GetDistinctAlarmIds();
    }
public interface IRepository<T>
         where T : IAggregateRoot
    {
        Task SaveAsync();
    }
 public async Task SaveAsync()
        {
            await UnitOfWork.CommitAsync();
        }

这是警报规范。

 public static class AlarmCodeSpecifications
    {
        public static ASpec<AlarmCode> ForAlarmId(short? alarmId)
        {
            return new Spec<AlarmCode>(o => o.AlarmId == alarmId);
        }
    }

Spec 和 ASpec 来自 https://github.com/jnicolau/NSpecifications 上的 NSpecifications 库: https://github.com/jnicolau/NSpecifications/blob/master/Nspecifications/ASpec.cs

【问题讨论】:

    标签: c# unit-testing moq xunit


    【解决方案1】:

    由于缺少信息,必须做出一些假设,但以下应该提供足够的平台来理解如何锻炼被测对象

    [Fact]
    public async Task Should_DeleteWithAlarmIdOne_WhenCalledWitParameterAlarmIdOne() {
        // Arrange
        short? expectedAlarmId = 1;
        var alarmCode = new AlarmCode { AlarmId = expectedAlarmId };
        var alarmCodes = new List<AlarmCode>(alarmCode);
    
        var repo = new Mock<IAlarmCodeRepository>();
        //fake the desired functionality
        repo.Setup(_ => _.DeleteAsync(It.IsAny<ASpec<AlarmCode>>()))
            .ReturnsAsync((ASpec<AlarmCode> arg) => alarmCodes.Where(arg));
        //allow async flow
        repo.Setup(_ => _.SaveAsync()).ReturnsAsync(Task.CompletedTask); //assuming it it void (Task)
    
        var command = new DeleteAlarmCodesCommand() { AlarmId = expectedAlarmId };
    
        var commandHandler = new DeleteAlarmCodesCommandHandler(repo.Object);
    
        // Act
        var result = await commandHandler.Handle(command, default(CancellationToken));
    
    
        // Assert
        var expected = Spec<AlarmCode>.Any & AlarmCodeSpecifications.ForAlarmId(command.AlarmId);
        repo.Verify(x => x.DeleteAsync(It.Is<ASpec<AlarmCode>>(actual => actual == expected)), Times.Once);
        repo.Verify(x => x.SaveAsync(), Times.Once);
    }
    

    【讨论】:

    • 谢谢!我想知道我现在怎么知道它完全称为 ForAlarmId(alarmId) 规范。 SaveAsync 确实是无效的。我将其更改为 repo.Setup(x =&gt; x.UnitOfWork.CommitAsync()).ReturnsAsync(true); SaveAsync 是 unitOfWork 周围的“包装器”。
    • 它是被模拟的接口,所以实现是什么并不重要。设置应在暴露SaveAsync的界面上完成
    • 由于在被测方法中构建了确切的规范,您将不得不推断我们构建的内容。这就是为什么将过滤器应用于已知匹配的集合
    • @Scathach 检查更新的替代方案,尽管我更喜欢我原来的建议
    • 谢谢你的解释。我现在看到了如何在其他测试中使用它。我将其标记为答案。
    猜你喜欢
    • 1970-01-01
    • 2015-05-31
    • 1970-01-01
    • 1970-01-01
    • 2016-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多