1

I am not sure ElasticSearch's .net low level client is commonly used yet, because NEST seems to be the "go to" approach to implement an Elasticsearch client in .net.

Anyway, among many other methods, the main IElasticLowLevelClient interface exposes a method to create indices :

ElasticsearchResponse<T> IndicesCreate<T>(
    string index, 
    PostData<object> body, 
    Func<CreateIndexRequestParameters, CreateIndexRequestParameters> requestParameters = null
) where T : class;

As explained in this doc, you can pass a string instead of an instance of PostData<object> thanks to one of the handy PostData's implicit operators :

public class PostData<T> : IPostData<T>, IPostData
{ 
     // ... code omitted
     public static implicit operator PostData<T>(string literalString);
}

I am unit testing a PersonIndexer class whose role is to call IndicesCreate with the correct index name "people" and the correct json body - I don't care about the last param. To do that I am mocking IElasticLowLevelClient using the Moq library.

Here is how PersonIndexer calls the low level client to create the index :

var result = _elasticClient.IndicesCreate<object>("people", "{ properties: {etc}}", null);

Here is what the PersonIndexerTest class verification would ideally look like (json parsing simplified):

_elasticClientMock.Verify(c => c.IndicesCreate<object>(
            "people",
            It.Is<string>(body =>
                JObject.Parse(body).SelectToken("$.properties.name.type").ToString() == "string"
            ), 
            null
        ));

The issue is that Moq never sees this call because it is expecting PostData<object> to match the second argument type, not a string.

You would tell me to use the "real" underlying type in my clause : It.Is<PostData<object>>(...) but this raises another issue : the PostData class does not publicly expose the string I used as the implicit constructor, thus it prevents me from parsing the body of my index creation request.

Is there any trick I can use to verify the string passed ? Is there any way to setup the mock so that it reproduces the implicit operator's behavior ? Am I going in the wrong direction...?

Sbu
  • 910
  • 11
  • 22
  • By the way, I just found out that `PostData` has an other implicit operator for byte arrays, and it has a different behavior. In fact, if instead of passing a string, I pass a byte array, the PostData instance will automatically make it available in its WrittenBytes property (it doesn't with the string operator). This is not very clean because it forces me to convert my nice string body expression to a bytearray in every indexer's implementation, but it's testable. I would prefer a mocking feature that bypasses the implicit operator. – Sbu Dec 09 '17 at 08:30
  • 1
    You can check testing frameworks like Typemock or one from Telerik, but Moq and NSubstitude won't be able to handle implicit conversion because it happens before argument is passed into the method and those frameworks can only intercept actual parameter. – Andrii Litvinov Dec 09 '17 at 08:37

1 Answers1

2

Provided you are not testing implementation of IElasticLowLevelClient the easiest approach to the problem would be to define you own interface for index creation that will accept index name and a string body. Use that interface from your code and assert correctness in test.

Alternatively you can use a bit of reflection to get private _literalString field in It.Is<PostData<object>>(...) statement.

UPDATE:

After investigating implementation of PostData I think I found better alernavive to reflection. You can setup callback for the method invocation, and the write PostData to a stream. Then you will be able to convert stream to a string and assert.

// Arrange
PostData<object> data = null;
mock
    .Setup(m => m.IndicesCreate<object>(It.IsAny<string>(), It.IsAny<PostData<object>>(), null))
    .Callback<string, PostData<object>>((s, d) => data = d);

// Act
// You test logic

// Assert
var stream = new MemoryStream();
data.Write(stream, fakeSettings);
var index = Enconding.UTF8.GetString(stream.ToArray());
index.Should().Be("{ properties: {etc}}");
Andrii Litvinov
  • 12,402
  • 3
  • 52
  • 59
  • I like your first solution's approach but I see this as a fallback solution because it mildly annoys me to abstract the low level client away only for unit testing reasons. – Sbu Dec 09 '17 at 08:34
  • 1
    @SimonBudin it is a trade-off as always. Unnecessary conversion of string to bytes will increase complexity of your client code. And it's also done for testing purpose. – Andrii Litvinov Dec 09 '17 at 08:40
  • Actually, I just took a shot at your second solution, reflexion and it finally makes good sense IMO. The "dirtiness" is isolated in tests, so it's acceptable. – Sbu Dec 09 '17 at 08:55
  • @SimonBudin personally I would go with reflection for start if it is used in a single test or at least extracted into a method to reuse in case it is needed in multiple tests. If the number of places where you create index in your codebase grows I would refactor to first approach over time. – Andrii Litvinov Dec 09 '17 at 09:03
  • @SimonBudin, I have updated my answer, I hope it makes better sense now. It there is not reflection involved. I haven't try to run it though, so not sure if I typed everything correctly. – Andrii Litvinov Dec 09 '17 at 09:38
  • 1
    Ah thanks, I will give this a try. Accepted answer because 3 solutions provided ! – Sbu Dec 09 '17 at 12:55
  • you shouldn't need to setup a callback for that: create a mock of IElasticLowLevelClient, call your logic injecting the mock and just use the Verify method on the mock to check if the method was called or not with expected parameters. – Romain Hautefeuille Sep 21 '18 at 15:58