elasticsearch-net icon indicating copy to clipboard operation
elasticsearch-net copied to clipboard

Unit Testing beef up

Open RejectKid opened this issue 1 year ago • 4 comments

I have noticed the new ESClient is a bit hard to unit test. I understand that not all features are in the new 8.x.x versions but it's seeming very difficult to test given the accessibility of things (unless im dumb, which is possible)

var resourcesFromElastic = await elasticSearchClient.Value.SearchAsync<Source>(searchRequest.Value);

if i try to mock the response using the TestableResponseFactory I dont see an easy way to create mock data to return.

var expectedSearchResponse = new SearchResponse<Source>() { HitsMetadata = new HitsMetadata<Source>() { Hits = new List<Hit<Source>>() { new(), new() } } };

this was the best i got so far, but I'd rather fill it in with data (my Source object) and I cant due to Hit, is there a way to handle this? or is this coming in the future

RejectKid avatar May 12 '23 14:05 RejectKid

Agreed, mocking is indeed a bit tricky.

The in-memory connection currently seems to work best for most of my scenarios. Unfortunately it isn't typed, but requires you to write the raw JSON as returned by Elasticsearch:

var response = @"{
    ""hits"": {
        ""hits"": [
            {
                // ...
            }
        ]
    }
}";
        
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var settings = new ConnectionSettings(pool, new InMemoryConnection(Encoding.UTF8.GetBytes(response)));
var client = new ElasticClient(settings);
        
var response = await client.SearchAsync(...);

// TODO: Assertions

You can find plenty examples of this pattern in the client tests themselves.

philipproplesch avatar May 12 '23 17:05 philipproplesch

Gotcha I see, i appreciate that. I dont believe that in memory connection should be the default.

image was able to do this so far. but the amount of hoops is a bit rough. circular depends in DTOs, read only fields, etc makes it pretty hard.

RejectKid avatar May 15 '23 14:05 RejectKid

@RejectKid We are working on some documentation for our recommendations. We have seen examples where users try to test at the wrong abstraction level, often repeating our testing. Generally, we recommend that you abstract our types and then include a thin unit/integration testing layer to ensure the mapping of data from our types to your own. That way, your application code can depend on the types you own and can fully control for testing.

That said, you should be able to mock the search responses as follows:

var expectedSearchResponse = new SearchResponse<MySource>()
{
	HitsMetadata = new HitsMetadata<MySource>()
	{
		Hits = new List<Hit<MySource>>()
		{
			new() { Source = new MySource{ Name= "Name1" } },
			new() { Source = new MySource{ Name= "Name2" } }
		}
	}
};

var mockedSearchResponse = TestableResponseFactory.CreateSuccessfulResponse(expectedSearchResponse, 200);

var mockedClient = Mock.Of<ElasticsearchClient>(e =>
	e.Search<MySource>() == mockedSearchResponse);

var testResponse = mockedClient.Search<MySource>();

Note that this only sets the source value on the hits and none of the metadata, including the total number of hits etc., which can be pretty significant on a search response. As @philipproplesch suggested, for complex responses, it's often better and recommended to capture the JSON example of a response and use the InMemoryConnection since that way, you get an entirely constructed response.

stevejgordon avatar May 15 '23 15:05 stevejgordon

@stevejgordon I created a wrapper to mock certain functionality in ElasticsearchClient like the below.

public class SearchClient : ISearchClient
{
	private readonly ElasticsearchClient _client;

	public SearchClient(ElasticsearchClient client)
	{
		_client = client;
	}

	public Task<SearchResponse<TDocument>> SearchAsync<TDocument>(Action<SearchRequestDescriptor<TDocument>> configureRequest, CancellationToken cancellationToken = default)
	{
		return _client.SearchAsync(configureRequest, cancellationToken);
	}

	public Task<TResponse> RequestAsync<TResponse>(HttpMethod method, string path, PostData data, RequestParameters parameters, CancellationToken token = default)
		where TResponse : TransportResponse, new()
	{
		return _client.Transport.RequestAsync<TResponse>(method, path, data, parameters, token);
	}
}

Unit test

var clientMock = new Mock<ElasticsearchClient>();
var searchClient = new SearchClient(clientMock.Object);

var method = HttpMethod.GET;
var path = "/index/_mapping";
var data = PostData.Serializable(new { });
var parameters = new RequestParametersMock();
var cancellationToken = new CancellationToken();

var response = new RequestResponseMock();
clientMock
	.Setup(x => x.Transport.RequestAsync<RequestResponseMock>(method, path, data, parameters, cancellationToken))
	.ReturnsAsync(response);

var result = await searchClient.RequestAsync<RequestResponseMock>(method, path, data, parameters, cancellationToken);

clientMock.Verify(
	x => x.Transport.RequestAsync<RequestResponseMock>(method, path, data, parameters, cancellationToken),
	Times.Once);

Assert.NotNull(result);

How do you mock properly the Transport?

virtualaidev avatar Jun 02 '23 06:06 virtualaidev