使用在 Task.Run 内部调用的 Moq 方法进行单元测试
Unit testing with Moq method invoked inside of Task.Run
我正在尝试在要测试的方法中模拟服务调用。
方法体如下所示:
public string OnActionException(HttpActionContext httpRequest, Exception ex)
{
var formattedActionException = ActionLevelExceptionManager.GetActionExceptionMessage(httpRequest);
var mainErrorMessage = $"[{formattedActionException.ErrorId}]{formattedActionException.ErrorMessage}, {ex.Message}";
this.LogError(mainErrorMessage, ex);
if (this._configuration.MailSupportOnException)
Task.Run(async () => await this._mailService.SendEmailForThrownException(this._configuration.SupportEmail, $"{mainErrorMessage} ---> Stack trace: {ex.StackTrace.ToString()}"));
return $"(ErrID:{formattedActionException.ErrorId}) {formattedActionException.ErrorMessage} {formattedActionException.KindMessage}";
}
我在测试中试图模拟的是:
Task.Run(async () => await this._mailService.SendEmailForThrownException(this._configuration.SupportEmail, $"{mainErrorMessage} ---> 堆栈跟踪:{ex.StackTrace.ToString()}"));
测试方法如下:
[TestMethod]
public void We_Send_System_Exception_On_Email_If_Configured_In_Settings()
{
// arrange
this._configurationWrapperMock.Setup(cwm => cwm.MailSupportOnException)
.Returns(true);
this._mailServiceMock.Setup(msm => msm.SendEmailForThrownException(It.IsAny<string>(), It.IsAny<string>()))
.Returns(Task.FromResult(0));
// act
var logger = new ApiLogger(this._configurationWrapperMock.Object, this._mailServiceMock.Object);
logger.OnActionException(
new HttpActionContext(
new HttpControllerContext()
{
Request = new HttpRequestMessage()
{
Method = HttpMethod.Get,
RequestUri = new System.Uri("https://www.google.bg/")
}
},
new ReflectedHttpActionDescriptor() { }
),
new System.Exception());
// assert
this._mailServiceMock.Verify(
msm => msm.SendEmailForThrownException(It.IsAny<string>(), It.IsAny<string>()),
Times.Once);
}
问题是该方法从未被调用,所以我的断言失败了。
编辑:我可以将我的问题更改为:我需要如何重写我的方法才能使其可测试?
我尝试了上面场景的一个简单的简化版本,并且单元测试始终通过。
public interface IService
{
Task<bool> Do();
}
public class AsyncService : IService
{
public async Task<bool> Do()
{
return await Task.FromResult(true);
}
}
public class MyClass
{
private IService service;
public MyClass(IService service)
{
this.service = service;
}
public async Task<bool> Run()
{
return await this.service.Do();
}
}
[TestMethod]
public async Task TestAsyncMethod()
{
Mock<IService> mockService = new Mock<IService>();
mockService.Setup(m => m.Do()).Returns(Task.FromResult(false));
MyClass myClass = new MyClass(mockService.Object);
await myClass.Run();
mockService.Verify(m => m.Do(), Times.Once);
}
看起来您需要将 OnActionException return 设置为 Task,然后还使单元测试异步。因此,不是使用 Task.Run(),而是 return OnActionException 方法中的 await this._mailService.SendEmailForThrownException()。
我想出的解决方案是提取新的服务方法,它是 Sync,在其中我使用 Thead.Run.
在单独的线程中私下调用我的异步方法
public void ReportExceptionOnEmail(string recipient, string exceptionBody)
{
Task.Run(async () => await this.SendEmailForThrownException(recipient, exceptionBody));
}
private async Task SendEmailForThrownException(string recipientEmail, string exceptionBody)
所以,现在我可以毫无问题地对 ReportExceptionOnEmail 方法进行单元测试。
您的代码是应用的经典情况Humble Object Pattern。
在这种情况下,您所要做的就是将 Task.Run
提取到虚拟方法中,然后部分模拟 SUT 并覆盖此虚拟方法:
public class ApiLogger
{
...
public string OnActionException(Exception ex)
{
...
if (this._configuration.MailSupportOnException)
RunInTask(...);
...
}
public virtual Task RunInTask(Action action)
{
return Task.Run(action);
}
}
然后测试将如下所示:
[TestMethod]
public void We_Send_System_Exception_On_Email_If_Configured_In_Settings()
{
...
var logger = new Mock<ApiLogger>(MockBehavior.Default,
new object[]
{
this._configurationWrapperMock.Object,
this._mailServiceMock.Object
}).Object;
logger.OnActionException(...);
this._mailServiceMock.Verify(
msm => msm.SendEmailForThrownException(It.IsAny<string>(), It.IsAny<string>()),
Times.Once);
}
我正在尝试在要测试的方法中模拟服务调用。
方法体如下所示:
public string OnActionException(HttpActionContext httpRequest, Exception ex)
{
var formattedActionException = ActionLevelExceptionManager.GetActionExceptionMessage(httpRequest);
var mainErrorMessage = $"[{formattedActionException.ErrorId}]{formattedActionException.ErrorMessage}, {ex.Message}";
this.LogError(mainErrorMessage, ex);
if (this._configuration.MailSupportOnException)
Task.Run(async () => await this._mailService.SendEmailForThrownException(this._configuration.SupportEmail, $"{mainErrorMessage} ---> Stack trace: {ex.StackTrace.ToString()}"));
return $"(ErrID:{formattedActionException.ErrorId}) {formattedActionException.ErrorMessage} {formattedActionException.KindMessage}";
}
我在测试中试图模拟的是:
Task.Run(async () => await this._mailService.SendEmailForThrownException(this._configuration.SupportEmail, $"{mainErrorMessage} ---> 堆栈跟踪:{ex.StackTrace.ToString()}"));
测试方法如下:
[TestMethod]
public void We_Send_System_Exception_On_Email_If_Configured_In_Settings()
{
// arrange
this._configurationWrapperMock.Setup(cwm => cwm.MailSupportOnException)
.Returns(true);
this._mailServiceMock.Setup(msm => msm.SendEmailForThrownException(It.IsAny<string>(), It.IsAny<string>()))
.Returns(Task.FromResult(0));
// act
var logger = new ApiLogger(this._configurationWrapperMock.Object, this._mailServiceMock.Object);
logger.OnActionException(
new HttpActionContext(
new HttpControllerContext()
{
Request = new HttpRequestMessage()
{
Method = HttpMethod.Get,
RequestUri = new System.Uri("https://www.google.bg/")
}
},
new ReflectedHttpActionDescriptor() { }
),
new System.Exception());
// assert
this._mailServiceMock.Verify(
msm => msm.SendEmailForThrownException(It.IsAny<string>(), It.IsAny<string>()),
Times.Once);
}
问题是该方法从未被调用,所以我的断言失败了。
编辑:我可以将我的问题更改为:我需要如何重写我的方法才能使其可测试?
我尝试了上面场景的一个简单的简化版本,并且单元测试始终通过。
public interface IService
{
Task<bool> Do();
}
public class AsyncService : IService
{
public async Task<bool> Do()
{
return await Task.FromResult(true);
}
}
public class MyClass
{
private IService service;
public MyClass(IService service)
{
this.service = service;
}
public async Task<bool> Run()
{
return await this.service.Do();
}
}
[TestMethod]
public async Task TestAsyncMethod()
{
Mock<IService> mockService = new Mock<IService>();
mockService.Setup(m => m.Do()).Returns(Task.FromResult(false));
MyClass myClass = new MyClass(mockService.Object);
await myClass.Run();
mockService.Verify(m => m.Do(), Times.Once);
}
看起来您需要将 OnActionException return 设置为 Task
我想出的解决方案是提取新的服务方法,它是 Sync,在其中我使用 Thead.Run.
在单独的线程中私下调用我的异步方法public void ReportExceptionOnEmail(string recipient, string exceptionBody)
{
Task.Run(async () => await this.SendEmailForThrownException(recipient, exceptionBody));
}
private async Task SendEmailForThrownException(string recipientEmail, string exceptionBody)
所以,现在我可以毫无问题地对 ReportExceptionOnEmail 方法进行单元测试。
您的代码是应用的经典情况Humble Object Pattern。
在这种情况下,您所要做的就是将 Task.Run
提取到虚拟方法中,然后部分模拟 SUT 并覆盖此虚拟方法:
public class ApiLogger
{
...
public string OnActionException(Exception ex)
{
...
if (this._configuration.MailSupportOnException)
RunInTask(...);
...
}
public virtual Task RunInTask(Action action)
{
return Task.Run(action);
}
}
然后测试将如下所示:
[TestMethod]
public void We_Send_System_Exception_On_Email_If_Configured_In_Settings()
{
...
var logger = new Mock<ApiLogger>(MockBehavior.Default,
new object[]
{
this._configurationWrapperMock.Object,
this._mailServiceMock.Object
}).Object;
logger.OnActionException(...);
this._mailServiceMock.Verify(
msm => msm.SendEmailForThrownException(It.IsAny<string>(), It.IsAny<string>()),
Times.Once);
}