【问题标题】:C# / Moq - How to force an exception and return a value in one stepC# / Moq - 如何强制异常并一步返回值
【发布时间】:2021-04-22 12:59:16
【问题描述】:

我有一个使用以下方法的存储库DoSomeWork

internal class MyRepository : IMyRepository
{
   public MyRepository(ILogger<MyRepository> logger, IDbContextWrapper dbContext)
   {
       this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
       this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
   }

   public Task<Result> DoSomeWork(int someInt)
   {
       return Task.Run(() =>
       {
           try
           {
               var parameters = new DynamicParameters(new { SomeIntValue = someInt });
               parameters.Add("WorkComplete", DbType.Boolean, direction: ParameterDirection.ReturnValue);
               dbContext.TransactionedExecute("NameOfStoredProcedure", parameters, CommandType.StoredProcedure); //This is a wrapper for Dapper (DbConnection)
               var status = (DoSomeWorkStatus)parameters.Get<int>("WorkComplete");
               var workComplete = status == DoSomeWorkStatus.DoneSuccessfully;

               return workComplete ? Result.WorkDone : Result.NoWorkDone;
           }
           catch(DatabaseTimeoutException dte)
           {
               logger.LogInformation(dte, "");
               return Result.Error;
           }
           catch(DatabaseDeadlockException dde)
           {
               logger.LogInformation(dde, "");
               return Result.Error;
           }
       });
    }
}

我想要实现的是测试和验证一旦 DatabaseTimeoutExceptionDatabaseDeadlockException 在 try/catch 中被捕获,任务应该返回 Result.Error。所有这一切都应该一步完成(无需重试)。

在测试中我有以下内容:

private Mock<IMyRepository> myRepoMock;
private MyRepoManager target;

...

[SetUp]
public void SetUp()
{
   myRepoMock = new Mock<IMyRepository>();
   target = new MyRepoManager(myRepoMock.Object);
}

[Test]
public async Task MyMoqTest()
{
    //Arrange
    myRepoMock
      .Setup(mrm => mrm.DoSomeWork(It.IsAny<int>()))
      .Returns(Task.FromException<Result>(new DatabaseTimeoutException()));

    //myRepoMock
    //  .Setup(mrm => mrm.DoSomeWork(It.IsAny<int>()))
    //  .Throws<DatabaseTimeoutException>(); <- The same result as above

    //Act
    Result taskResult = await target.RunTask(int someInt); //Calls repository method - DoSomeWork

    //Assert
    Assert.AreEqual(Result.Error, taskResult.Result);
}

但是发生的情况是存储库抛出 DatabaseTimeoutException 而不返回 Result.Error,并且测试失败并显示消息(如预期的那样):

MyExceptions.DatabaseTimeoutException : Exception of type 'MyExceptions.DatabaseTimeoutException' was thrown.

我对 Moq 很陌生,所以我的问题是 - 这可以用 Moq 完成吗?如果可以,我将如何去做?

谢谢。

【问题讨论】:

  • 您似乎误解了 mocking 的工作原理。您不需要“强制例外”。您所要做的就是模拟行为 - 这意味着您可以让您的模拟返回您想要的结果(Result.Error),而无需涉及异常。
  • 我想添加到@mason 的答案,如果你想测试 MyRepository 中的一些逻辑 - 你不应该模拟那个类。您应该创建一个实际的实现,但要使用模拟的依赖项(DbContext 和 Logger)。如果你想测试 catch 中的行为,你可以设置 DbContext mock 来抛出异常,然后调用 MyRepository.DoSomeWork 并检查它会返回什么。
  • 谢谢@GoodboY,我做到了,它按预期工作。

标签: c# moq


【解决方案1】:

单元测试最重要的部分是识别被测系统 (SUT)。这就是你实际上要验证作品的东西。一旦你确定了这一点,你的 SUT 的所有依赖项都应该被模拟,这样你就可以严格控制你正在测试的东西之外的所有东西。

如果您尝试对 MyRepoManager.RunTask 进行单元测试,那么它不应该关心其依赖项的任何内部实现细节。它应该只关心他们暴露的合同。在这种情况下,您依赖于 IMyRepository。因此,具体实现 MyRepository 的作用无关紧要。 MyRepository 可能会在内部处理 DatabaseTimeoutException 和 DatabaseDeadlockException,但这是一个实现细节,而不是通过 IMyRepository 定义的合同的一部分。目标是模拟依赖项的行为,而不是在模拟框架内完全重新实现依赖项的内部行为。

所以,你的模拟设置应该是:

myRepoMock
    .Setup(mrm => mrm.DoSomeWork(It.IsAny<int>()))
    .Returns(Task.FromResult(Result.Error));

【讨论】:

  • 感谢 @mason 为我清理它。我继续修改我的代码,以便我现在模拟 dbContext 而不是存储库,并获得我需要的结果:)
猜你喜欢
  • 1970-01-01
  • 2014-02-10
  • 1970-01-01
  • 1970-01-01
  • 2010-11-06
  • 2016-04-22
  • 2020-07-14
  • 2016-01-14
相关资源
最近更新 更多