Mocking the .NET Client for ExistsResponse in unit tests

Hello Everyone,

I am using .NET 8 and "Elastic.Clients.Elasticsearch" Version="8.15.6" (not NEST)
For mocking we are using NSubstitute(not Moq)

I tried to go through multiple blogs and even the github repo for .NET client tests but could not find anything concrete around how to mock the sealed class for exists response i.e. public sealed partial class ExistsResponse : ElasticsearchResponse

We could successfully use it for SearchResponse and MappingResponse but could not make it work for ExistsResponse

var searchResponse = new SearchResponse<Dictionary<string, object>>();
var response = TestableResponseFactory.CreateSuccessfulResponse(searchResponse, 200);
or
var mockIndexMappingRecord = new IndexMappingRecord
{
    Mappings = mockTypeMapping // The mappings we just defined
 };
var mockDictionary = new Dictionary<IndexName, IndexMappingRecord>
{
  { "indexName", mockIndexMappingRecord }
 };
var getMappingResponse = TestableResponseFactory.CreateResponse(
        new GetMappingResponse(mockDictionary),
        200, // HTTP 200 OK from Elasticsearch
        true // Valid response flag
      );

Below are the options we tried and failed

  1. TestableResponseFactory - This gives the compile error for Exists = true
var responsee = TestableResponseFactory.CreateResponse(
  new ExistsResponse
  {
    Exists = true
  },
  200, // HTTP 200 OK
  true // Valid response flag
);
_elasticClient
            .ExistsAsync(
              Arg.Any<Elastic.Clients.Elasticsearch.Indices>(), // Match any Indices argument
              Arg.Any<System.Threading.CancellationToken>() // Match any CancellationToken argument
            )
            .Returns(Task.FromResult(responsee));
  1. Having a ReflectionWrapper. For reflection its complicated the things.
var apiCallDetails = ReflectionWrapper.CreateInstanceWithPrivateConstructor<ApiCallDetails>();
ReflectionWrapper.SetPrivateFieldOrProperty(apiCallDetails, "HttpStatusCode", 200);
var response = new ExistsResponse();
ReflectionWrapper.SetPrivateFieldOrProperty(response, "ApiCallDetails", apiCallDetails);
_elasticClient
            .ExistsAsync(
              Arg.Any<Elastic.Clients.Elasticsearch.Indices>(),
              Arg.Any<System.Threading.CancellationToken>()
            )
            .Returns(Task.FromResult(response)); // Return the Task-wrapped ExistsResponse
var result = await _elasticClient.ExistsAsync(
            Arg.Any<Elastic.Clients.Elasticsearch.Indices>(),
            Arg.Any<System.Threading.CancellationToken>()
          );
bool exists = result.Exists;

We get runtime errors:

System.InvalidOperationException: Field or property 'HttpStatusCode' not found on type 'Elastic.Transport.ApiCallDetails'.
System.InvalidOperationException
Field or property 'HttpStatusCode' not found on type 'Elastic.Transport.ApiCallDetails'.

Had also tried the in-memory connection though not recommended.
In-memory git link :

Can some one guide how to proceed?

Regards,
Moni

Hi @Moni_Hazarika ,

Just a few words related to this style of testing in general:

IMO you are mocking on the wrong abtraction level. Currently, you are trying to mimic the Elasticsearch client/server behavior - which in the end does not really verify anything meaningful.

Do you have some kind of repository pattern in your application? If yes, then this would be the place where I could imagine using mocks.

As for your concrete problem:

Reflection will be the only way to mock this response and they way you are trying it looks good to me. I'm not sure why your reflection helpers can't find the HttpStatusCode property as I don't know your implementation.

All the code related to Elastic.Transport is available here:

ApiCallDetails.HttpStatusCode is a public property with an internal setter. Maybe your reflection setter does the wrong thing here?

Thanks @flobernd for your response.

Yes we have the repository pattern and need to mock the Elasticsearch client and response to that level.
We made some changes to the way we called reflection helpers and now we have passed through the invalid operation exception. Below is the latest code where we have combined Reflection with ResponseFactory.

// Step 1: Create an instance of ApiCallDetails using private/internal constructor
          var apiCallDetails = ReflectionWrapper.CreateInstanceWithPrivateConstructor<ApiCallDetails>();

          // Step 2: Set 'HttpStatusCode' to 200 (which will cause 'HasSuccessfulStatusCode' to return true)
          ReflectionWrapper.SetPrivateFieldOrProperty(apiCallDetails, "HttpStatusCode", 200);

          // Step 3: Set 'HasSuccessfulStatusCode' to true
          ReflectionWrapper.SetPrivateFieldOrProperty(apiCallDetails, "HasSuccessfulStatusCode", true);

          // Step 4: Create an instance of ExistsResponse
          var response = new ExistsResponse();

          // Step 5: Inject ApiCallDetails into ExistsResponse using reflection
          ReflectionWrapper.SetPrivateFieldOrProperty(response, "ApiCallDetails", apiCallDetails);

          //ReflectionWrapper.SetPrivateFieldOrProperty(response, "IsValidResponse", true);

          var existresponse = TestableResponseFactory.CreateResponse(
            response,
            200, // HTTP 200 OK from Elasticsearch
            true // Valid response flag
          );

          // Step 6: Mock ExistsAsync for the indices and cancellation token
          _elasticClient
            .ExistsAsync(Arg.Any<Elastic.Clients.Elasticsearch.Indices>(), Arg.Any<System.Threading.CancellationToken>())
            .Returns(Task.FromResult(existresponse));

Currently when the test class hits the actual call here
await _elasticClient.Indices.ExistsAsync(IndexName)
We are now getting null response. If you have mocked the client and response correctly then it should be using our instance and SHOULD NOT give me null.
Due to which test case fails with NPE as further down the line we have references using response.Exists.

Our sample reflection wrapper class below:

public static class ReflectionWrapper
    {
      /// <summary>
      /// Creates an instance of an object using its non-public constructor.
      /// </summary>
      /// <typeparam name="T">The type of object to create.</typeparam>
      /// <returns>The created object instance.</returns>
      public static T CreateInstanceWithPrivateConstructor<T>() where T : class
      {
        var type = typeof(T);
        var constructorInfo = type.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);

        if (constructorInfo == null)
        {
          throw new InvalidOperationException($"No private or internal constructor found for type '{type}'.");
        }

        // Create an instance using the constructor
        return (T)constructorInfo.Invoke(null);  // No parameters needed for the constructor
      }

      /// <summary>
      /// Sets a private or internal field or property on an object using reflection.
      /// </summary>
      /// <param name="obj">The object instance.</param>
      /// <param name="propertyName">The name of the private/internal property or field to set.</param>
      /// <param name="value">The value to set.</param>
      public static void SetPrivateFieldOrProperty<T>(T obj, string propertyName, object value)
      {
        var type = obj?.GetType();

        // Try to find a Property first
        var propertyInfo = type?.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
        if (propertyInfo != null && propertyInfo.CanWrite)
        {
          propertyInfo.SetValue(obj, value);
          return;
        }

        // If property was not found, try to find a field
        var fieldInfo = type?.GetField(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
        if (fieldInfo != null)
        {
          fieldInfo.SetValue(obj, value);
          return;
        }

        throw new InvalidOperationException($"Field or property '{propertyName}' not found on type '{type}'.");
      }
    }