最近、MediatRの使用を開始して、大規模な顧客向けポータルをリファクタリングし、すべてをC#に変換するときに、コントローラーアクションを整理できるようにしました。その一環として、ユニットテストのカバレッジも拡大していますが、MediatR自体をモックしようとすると問題が発生しました。
コマンドはプロセスを開始するためにたくさんのことを行い、その一部は通知を送信します。通知自体は独自のハンドラーによって処理されるため、独自の単体テストの対象となるため、MediatRをモックして、this.mediator.Send(message)
呼び出しが実際には何もしないようにします。ハンドラーはオブジェクトを返しますが、このコンテキストでは気にしないので、すべての意図と目的で、オブジェクトをvoid
リターンとして扱います。テストの一環としてSend
が一度呼び出されたことを確認したいだけです。ただし、Send
メソッドはNullReferenceException
をスローしているため、理由はわかりません。
バージョン3以降、MediatRはSend
、CancellationToken
で2番目のオプションのパラメーターを取り、式ツリーでは明示的に設定する必要があるため、値を指定する必要があります。私はこれまでこれに遭遇したことがなく、私の心の中でこれは問題の一部かもしれないと感じていますが、これは私の側の混乱かもしれません。
これが切り詰められたイラストです。
SUT
public class TransferHandler : IAsyncRequestHandler<TransferCommand, TransferResult>
{
private readonly IMediator mediator;
public TransferHandler(IMediator mediator)
{
this.mediator = mediator;
}
public async Task<TransferResult> Handle(TransferCommand message)
{
// Other stuff.
var notification = new TransferNotificationCommand()
{
ClientId = message.clientId,
OfficeId = message.OfficeId,
AuthorityFileId = letter?.Id
};
await this.mediator.Send(notification); // <=== This is where we get a NullReferenceException, even though nothing is actually null (that I can see).
return new TransferResult()
{
Transfer = transfer,
FileId = letter?.Id
}
}
}
テスト
public class TransferHandlerTests
{
[Theory]
[AutoData]
public async void HandlerCreatesTransfer(Mock<IMediator> mockMediator)
{
// Note that default(CancellationToken) is the default value of the optional argument.
mockMediator.Setup(m => m.Send(It.IsAny<TransferNotificationCommand>(), default(CancellationToken))).Verifiable("Notification was not sent.");
var handler = new TransferHandler(mockMediator.Object);
var actual = await handler.Handle(message);
mockMediator.Verify(x => x.Send(It.IsAny<CreateIsaTransferNotificationCommand>(), default(CancellationToken)), Times.Once());
}
}
何が足りないのですか?どこかで根本的なミスをしたような気がしますが、どこかわかりません。
Send
メソッドがタスクを返すときに、非同期操作の待機を処理する必要があります。
/// <summary>
/// Asynchronously send a request to a single handler
/// </summary>
/// <typeparam name="TResponse">Response type</typeparam>
/// <param name="request">Request object</param>
/// <param name="cancellationToken">Optional cancellation token</param>
/// <returns>A task that represents the send operation. The task result contains the handler response</returns>
Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Asynchronously send a request to a single handler without expecting a response
/// </summary>
/// <param name="request">Request object</param>
/// <param name="cancellationToken">Optional cancellation token</param>
/// <returns>A task that represents the send operation.</returns>
Task Send(IRequest request, CancellationToken cancellationToken = default(CancellationToken));
つまり、非同期プロセスがフローを続行できるようにするには、モックにタスクを返す必要があります。
mediator
.Setup(m => m.Send(It.IsAny<TransferNotificationCommand>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Notification()) //<-- return Task to allow await to continue
.Verifiable("Notification was not sent.");
//...other code removed for brevity
mediator.Verify(x => x.Send(It.IsAny<CreateIsaTransferNotificationCommand>(), It.IsAny<CancellationToken>()), Times.Once());