В C#, почему один макет (Moq) не возвращает то, что должен?

#c# #unit-testing #dependency-injection #moq #return-value

#c# #модульное тестирование #инъекция зависимости #moq #возвращаемое значение

Вопрос:

У меня есть следующий код, который я хочу протестировать:

 // Snipped for brevity  public DocuSignCallbackHandler(  ContractDAO contractDAO,  ContractLister contractLister,  SalesforceOpportunityProvider salesforceOpportunityProvider,  IEnvelopeService docuSignEnvelopeService,  IHttpContextAccessor httpContextAccessor,  IImageSaver imageSaveClient,  ISalesforceCacheSyncDataManipulator salesforceCacheSyncDataManipulator,  SalesforceContractResolver salesforceContractResolver,  SfContractWriteDataFactory sfContractWriteFactory  )  {  _contractDAO = contractDAO ?? throw new ArgumentNullException(nameof(contractDAO));  _contractLister = contractLister ?? throw new ArgumentNullException(nameof(contractLister));  _salesforceOpportunityProvider = salesforceOpportunityProvider ?? throw new ArgumentNullException(nameof(salesforceOpportunityProvider));  _docuSignEnvelopeService = docuSignEnvelopeService ?? throw new ArgumentNullException(nameof(docuSignEnvelopeService));  _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));  _imageSaveClient = imageSaveClient ?? throw new ArgumentNullException(nameof(imageSaveClient));  _salesforceCacheSyncDataManipulator = salesforceCacheSyncDataManipulator ?? throw new ArgumentNullException(nameof(salesforceCacheSyncDataManipulator));  _salesforceContractResolver = salesforceContractResolver ?? throw new ArgumentNullException(nameof(salesforceContractResolver));  _sfContractWriteFactory = sfContractWriteFactory ?? throw new ArgumentNullException(nameof(sfContractWriteFactory));  }    public async Tasklt;ContractBasegt; UpdateDocuSignStatus(string contractNumber, CancellationToken cancellationToken)  {  ContractBase contract = await _contractLister.GetByContract(contractNumber)  ?? throw new IllegalContractOperationException($"Service does not have a contract numbered { contractNumber }.");   DocusignPdfResult docuSignPdfResult = await GetPDFfromDocuSign(contract, 0, cancellationToken);   contract = docuSignPdfResult.ImageLoadId != ""  ? AddSignature(contract, docuSignPdfResult)  : throw new IllegalContractOperationException($"Result is in an unexpected state: StatusCode: {docuSignPdfResult.StatusCode}, ImageLoadId: {docuSignPdfResult.ImageLoadId}");   contract.SfCacheCtrId = docuSignPdfResult.SalesforceContractId;   ContractBase result = await _contractDAO.Update(contract, cancellationToken);   // NOTE: In real life, this works,   // but in my unit tests, this value is always null.  return result;  }  // Snipped for brevity  

Пожалуйста, не обращайте внимания на то, что у этого класса слишком много зависимостей; я это знаю.

Мой тест выглядит так:

 [Fact]  public async Task TestUpdateDocuSignStatusShouldMarkContractsSigned()  {  // Arrange  AutoMocker mocker = new();  DocuSignCallbackHandler handlerUnderTest = mocker.CreateInstancelt;DocuSignCallbackHandlergt;();  ContractBase testContract = new()  {  ContractNumber = ContractNumber,  EnvelopeId = EnvelopeId,  ContractHistory = new()  {  new()  {  User = "Fred"  }  }  };  mocker.GetMocklt;ContractListergt;()  .Setup(x =gt; x.GetByContract(ContractNumber))  .ReturnsAsync(testContract);  mocker.GetMocklt;IEnvelopeServicegt;()  .Setup(x =gt; x.GetEnvelope(EnvelopeId, It.IsAnylt;Funclt;SessionUrlsgt;gt;()))  .ReturnsAsync(new MemoryStream());  mocker.GetMocklt;IImageSavergt;()  .Setup(x =gt; x.Save(It.IsAnylt;ImageEnvelopegt;(), CancellationToken))  .ReturnsAsync(new HttpResponseMessage()  {  StatusCode = HttpStatusCode.OK,  Content = new StringContent(JsonSerializer.Serialize(new Listlt;ImageUploaderSaveResponsegt;() {  new()  {  Id = ImageLoadId  }  }))  });  SfOpportunity testOpportunity = new()  {  AccountId = AccountId  };  mocker.GetMocklt;SalesforceOpportunityProvidergt;()  .Setup(x =gt; x.GetOpportunity(testContract, CancellationToken))  .ReturnsAsync(testOpportunity);  SfContract testOutboundContract = new();  mocker.GetMocklt;SfContractWriteDataFactorygt;()  .Setup(x =gt; x.CreateFor(testContract, testOpportunity, It.IsAnylt;ImageUploaderSaveResponsegt;()))  .Returns(testOutboundContract);  mocker.GetMocklt;SalesforceContractResolvergt;()  .Setup(x =gt; x.ToJObject(testOutboundContract))  .Returns(new JObject());  Listlt;SaveResponsegt; testSaveResponses = new()  {  new()  };  mocker.GetMocklt;ISalesforceCacheSyncDataManipulatorgt;()  .Setup(x =gt; x.Insert(It.IsAnylt;IListlt;SalesforceRecordgt;gt;(), It.IsAnylt;DataManipulationOptionsgt;(), CancellationToken))  .ReturnsAsync(testSaveResponses);   ContractBase finalContract = new()  {  ContractNumber = "2325235325"  };   // NOTE: This mock seems to be ignored.  mocker.GetMocklt;ContractDAOgt;()  .Setup(x =gt; x.Update(It.IsAnylt;ContractBasegt;(), It.IsAnylt;CancellationTokengt;()))  .ReturnsAsync(finalContract);   // Act  ContractBase resultContract = await handlerUnderTest.UpdateDocuSignStatus(ContractNumber, CancellationToken);   // Assert  Assert.Equal(finalContract, resultContract);  }  

Usually, Moq is working as expected, and my mocked out dependency methods are returning the values I want.

However the last mock within the «arrange» section seems to be ignored. As in the current incarnation, leveraging It.IsAny() for both parameters, I would expect it should always return the value of finalContract , but in fact, the return value of ContractDAO.Update() is always null , exactly the same as if I leave that mock out of the test code.

For whatever it may be worth, the concrete method I wish to mock out looks like this:

 // Snipped for brevity  public ContractDAO(ContractDatabaseSettings contractDatabaseSettings, ILoggerlt;ContractDAOgt; logger, IMongoDatabase mongoDatabase)  {  _contractDatabaseSettings = contractDatabaseSettings ?? throw new ArgumentNullException(nameof(contractDatabaseSettings));  _logger = logger ?? throw new ArgumentNullException(nameof(logger));  _mongoDatabase = mongoDatabase ?? throw new ArgumentNullException(nameof(mongoDatabase));  }    public async Tasklt;ContractBasegt; Update(ContractBase inputContract, CancellationToken cancellationToken)  {  try  {  await GetCollection()  .ReplaceOneAsync(  contract =gt; contract.Id == inputContract.Id,  inputContract,  cancellationToken: cancellationToken  );  }  catch (Exception ex)  {  _logger.LogError(ex, "Tryng to save data for Contract {ContractNumber}.", inputContract.ContractNumber);  return null;  }  return inputContract;  }   private IMongoCollectionlt;ContractBasegt; GetCollection() =gt;  _mongoDatabase.GetCollectionlt;ContractBasegt;(_contractDatabaseSettings.ContractCollectionName);  // Snipped for brevity  

Как предложил @Ermiya Eskandary, я действительно могу исправить это, изменив порядок, в котором вещи высмеиваются, например, сделав это первой, а не последней высмеиваемой вещью, например:

 [Fact]  public async Task TestUpdateDocuSignStatusShouldMarkContractsSigned()  {  // Arrange  AutoMocker mocker = new();  ContractBase finalContract = new()  {  ContractNumber = "2325235325"  };   mocker.GetMocklt;ContractDAOgt;()  .Setup(x =gt; x.Update(It.IsAnylt;ContractBase?gt;(), It.IsAnylt;CancellationTokengt;()))  .ReturnsAsync(finalContract);  // etc.  

Чем эта зависимость отличается от любой другой зависимости, для которой насмешка всегда работает так, как ожидалось?

Почему заказ должен иметь значение? Поскольку это последний макет, который будет применен, почему он должен быть перечислен первым (или, по крайней мере, не последним)?

Комментарии:

1. Это всегда «последняя» насмешка? Пожалуйста, измените порядок — это имеет значение? Я не думаю, что это сработает, но, эй, это предложение. Также не могли бы вы добавить, пожалуйста, то, что ContractDAO выглядит (достаточно минимально, чтобы проблема все еще воспроизводилась)?

2. @ErmiyaEskandary, спасибо за быстрый ответ. Я не думал, что смена порядка сработает, но на самом деле я сделал это первым, над чем издевался, а не последним, и это сработало!!!! Если вы хотите опубликовать это в качестве ответа, я могу принять его, хотя мне хотелось бы знать, почему. Что касается ContractDAO, хотя я и не думаю, что в этом не должно быть необходимости, поскольку мы хотим его высмеять, я добавлю это выше.

3. Хм — не за что! В этом случае вернитесь к оригиналу, а затем переместите инициализацию handlerUnderTest дальше вниз — она же инициализация обработчика после того, как вы издевались над всеми своими объектами. Это все еще работает? И есть ли ContractDAO у самого какие-либо зависимости?

4. Я восстановил первоначальный порядок, за исключением того, что перешел DocuSignCallbackHandler handlerUnderTest = mocker.CreateInstancelt;DocuSignCallbackHandlergt;(); на последний, и да, это все еще работает. 🙂 Что касается зависимостей от контракта, да, я добавил их к вопросу выше.

5. Я так и думал! Скоро напишу ответ, чтобы удовлетворить любопытство 🙂