如何在 C# 单元测试中验证具有特定 NSpecification 参数的方法?
How can you verify a method with a specific NSpecification parameter in C# unit tests?
我目前正在为 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 是否有办法验证复杂对象的调用,例如 Spec。
或者我怎样才能更好地测试它?
更新
这是 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
由于缺少信息,必须做出一些假设,但以下内容应该提供足够的平台来理解如何练习被测主题
[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);
}
我目前正在为 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 是否有办法验证复杂对象的调用,例如 Spec。
或者我怎样才能更好地测试它?
更新
这是 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
由于缺少信息,必须做出一些假设,但以下内容应该提供足够的平台来理解如何练习被测主题
[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);
}