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
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));
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 :
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?
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}'.");
}
}
Apache, Apache Lucene, Apache Hadoop, Hadoop, HDFS and the yellow elephant
logo are trademarks of the
Apache Software Foundation
in the United States and/or other countries.