【问题标题】:Unit testing with Moq method invoked inside of Task.Run使用在 Task.Run 内部调用的 Moq 方法进行单元测试
【发布时间】:2017-11-29 20:28:12
【问题描述】:

我正在尝试在要测试的方法中模拟服务调用。

方法体如下所示:

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);
}

问题是该方法从未被调用,所以我的断言失败了。

编辑:我可以将我的问题改为:我需要如何重写我的方法以使其可测试?

【问题讨论】:

  • 您是否尝试过调试测试以查看代码是否通过预期路径?
  • 任务正在运行,只是在另一个线程中,所以你无法验证它。
  • @KerriBrown 它确实超过了这条线,我对其进行了调试,但起订量没有检测到它已通过。 Moq 说它从未被调用过。
  • 因为它从未在调用验证的线程上调用过。
  • @user2128702 你没有在 IDE 中收到关于任务在单独线程上执行的绿色波浪形警告吗?

标签: c# unit-testing mocking moq assert


【解决方案1】:

您的代码是应用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);
}

【讨论】:

    【解决方案2】:

    我尝试了上述场景的简单缩减版本,并且单元测试始终通过。

    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 返回一个 Task 然后让单元测试也异步。所以与其使用Task.Run(),不如在OnActionException方法中返回await this._mailService.SendEmailForThrownException()。

    【讨论】:

    • 我会接受你的回答。我以前尝试过,我知道它会起作用。我只是好奇是否有可能使我目前的情况发挥作用。
    【解决方案3】:

    我想出的解决方案是提取新的服务方法,即 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 方法进行单元测试。

    【讨论】:

      猜你喜欢
      • 2012-06-01
      • 2021-10-15
      • 1970-01-01
      • 2013-07-08
      • 2011-12-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多