How to mock SearchClient.SearchAsync with the Azure Cognitive Search SDK?
Asked Answered
S

1

7

I am trying to unit test code that uses SearchClient.SearchAsync() method. I am using AutoFixture.AutoMoq nuget package.

Here is what I tried:

mockSearchClient.Setup(msc => msc.SearchAsync<MyModel>(
        It.IsAny<string>(),
        It.IsAny<SearchOptions>(),
        It.IsAny<CancellationToken>()
    )).Returns(Task.FromResult(<<PROBLEM HERE>>));

The problem lies in the parameter .Returns(Task.FromResult(<<PROBLEM HERE>>)) part. It expects a concrete object that is returned from the .SearchAsync() method. According to docs and autocomplete, the method returns Azure.Response which is an abstract class. So, I cannot new it up. In actuality, the method returns a descendant class Azure.ValueResponse, which isn't abstract, but is internal to Azure SDK, so also impossible to new up.

So how does one mock the SearchClient.SearchAsync?

P.S. Using Azure.Search.Documents, v11.1.1.0

Sluggish answered 1/10, 2020 at 16:57 Comment(2)
And you can't create your own fake derived from the abstract response class?Affiche
@BertrandLeRoy The mocking frameworks generally require you to provide an instance. So I tried creating my own class that inherited from Azure.Response (class foo: Azure.Response {}), but that didn't work either. If you have a different way to do it, I'd love to try it.Sluggish
C
15

See https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/README.md#mocking for information. Basically, you can use Response.FromValue along with the SearchModelFactory (a pattern we follow with all our Azure.* client SDKs for models that can't be fully mocked with a constructor and/or settable properties) to create a mock like so (using Moq, since I'm unfamiliar with AutoMoq, but should be similar):

var responseMock = new Mock<Response>();

var clientMock = new Mock<SearchClient>(() => new SearchClient(new Uri("https://localhost"), "index", new AzureKeyCredential("key")));
clientMock.SetupGet(x => x.IndexName).Returns("index");
clientMock.Setup(x => x.SearchAsync<Hotel>(
        It.IsAny<string>(),
        It.IsAny<SearchOptions>(),
        It.IsAny<CancellationToken>()
    ))
    .Returns(
        Task.FromResult(
            Response.FromValue(
                SearchModelFactory.SearchResults(new[]
                    {
                        SearchModelFactory.SearchResult(new Hotel("1", "One"), 0.9, null),
                        SearchModelFactory.SearchResult(new Hotel("2", "Two"), 0.8, null),
                    },
                    100,
                    null,
                    null,
                    responseMock.Object),
                responseMock.Object)));

var results = await clientMock.Object.SearchAsync<Hotel>("test").ConfigureAwait(false);
var hotels = results.Value;

Assert.Equal(2, hotels.GetResults().Count());
Assert.Equal(100, hotels.TotalCount);
Caulfield answered 1/10, 2020 at 23:39 Comment(3)
Just wanted to add that the delegate form used in new Mock<SearchClient> is only necessary because of a bug that has since been fixed: github.com/Azure/azure-sdk-for-net/pull/15671. Normally, you can just use the construct that takes parameters passed through to the client e.g. SearchClient.Caulfield
Thanks @heath, this helped me find TableModelFactory to mock the TableServiceClient response modelSupra
FWIW: If newing up a SearchClient is cumbersome one can inherit like class FakeSearchClient : SearchClient and mock it through Mock<SearchClient>(() => new FakeSearchClient()).Bead

© 2022 - 2024 — McMap. All rights reserved.