Search code examples
c#.netunit-testingelasticsearchnest

Test for parameter passed to NEST (elasticsearch)


I am using NEST to do elasticsearch queries.

    public IReadOnlyCollection<IHit<Recommendation>> GetRecommendations(
        RecommenderQueryFields shoulds,
        RecommenderQueryFields musts, 
        RecommenderQueryFields mustNots)
    {
        var boolQuery = new BoolQuery();
        boolQuery.Should = GetQueryContainers(shoulds);
        boolQuery.Must = GetQueryContainers(musts);
        boolQuery.MustNot = GetQueryContainers(mustNots);

        var response = _elasticClient.Search<Recommendation>(s => s
            .AllTypes().From(0).Size(10)
            .Query(outerQuery => boolQuery));

        return response.Hits;
    }

I have logic in the GetQueryContainers method which I want to test. Is there some way to check what has been passed to the ElasticClient in the boolQuery object?

I have tried the following stuff already, using NUnit and NSubstitute.

    [Test]
    public void Test1()
    {
        // Arrange
        var searchResponse = Substitute.For<ISearchResponse<Recommendation>>();
        searchResponse.Hits.Returns(new List<IHit<Recommendation>>());
        var elasticClient = Substitute.For<IElasticClient>();

        var sut = new Recommender(elasticClient);

        // Act
        sut.GetRecommendations(null, null, null);

        // Assert
        elasticClient
            .Received(1)
            .Search(Arg.Is<Func<SearchDescriptor<Recommendation>, ISearchRequest>>(x => true));
    }

In the Arg.Is<[...]>(x => true) I would like to replace the true constant for some checks on boolQuery. But I do not know if it is possible or how it is done. Or is there a different way to do this?


Solution

  • TL;DR Use a derived QueryVisitor. See Edit2 below.

    Found that the question has been answered already. It is not related to NEST, but to testing lambda expressions. It is not possible: C# Moq Unit Testing with Lambda Expression or Func delegate

    What can be done is testing the JSON request which will be sent to elasticsearch, but then you need the actual ElasticClient: ElasticSearch NEST 5.6.1 Query for unit test

    What can be done is putting the logic in its own method/class. But then you write code simply for the sake of testing, which I'm not a fan of. Like:

    public BoolQuery GetBoolQuery(RecommenderQueryFields shoulds, RecommenderQueryFields musts,
        RecommenderQueryFields mustNots)
    {
        var boolQuery = new BoolQuery();
        boolQuery.Should = GetQueryContainers(shoulds);
        boolQuery.Must = GetQueryContainers(musts);
        boolQuery.MustNot = GetQueryContainers(mustNots);
        return boolQuery;
    }
    
    

    You are exposing a public method which you are not intending for use, only for testing. But you can then assert on boolQuery like this:

    [Test]
    public void GetRecommendations_CallsElasticSearch()
    {
        // Arrange
        var elasticClient = Substitute.For<IElasticClient>();
        var sut = new Recommender(elasticClient);
    
        // Act
        var boolQuery = sut.GetBoolQuery(new RecommenderQueryFields{BlackListedFor = new List<string>{"asdf"}}, null, null);
    
        // Assert
        Assert.AreEqual(1, boolQuery.Should.Count());
    }
    

    In boolQuery.Should is a list of QueryContainer which are not testable because it is generated with lambdas aswell. While better than nothing, it is still not a clean way to test NEST.

    Edit

    @Russ Cam in the comment has mentioned the IQueryContainer and QueryVisitor What I've got:

    [Test]
    public void test()
    {
        // Arrange
        var fieldValue = "asdf";
        var elasticClient = Substitute.For<IElasticClient>();
        var sut = new Recommender(elasticClient);
    
        // Act
        var boolQuery = sut.GetBoolQuery(new RecommenderQueryFields { BlackListedFor = new List<string> { fieldValue } }, null, null);
    
        // Assert
        IQueryContainer qc = boolQuery.Should.First(); // Cast to IQueryContainer
        Assert.AreEqual(fieldValue, qc.Match.Query); // Assert value
    
        // Get "field name"
        var queryVisitor = new QueryVisitor();
        var prettyVisitor = new DslPrettyPrintVisitor(new ConnectionSettings(new InMemoryConnection()));
        qc.Accept(queryVisitor);
        qc.Accept(prettyVisitor);
        Assert.AreEqual(0, queryVisitor.Depth);
        Assert.AreEqual(VisitorScope.Query, queryVisitor.Scope);
        Assert.AreEqual("query: match (field: blacklistedfor.keyword)\r\n", prettyVisitor.PrettyPrint);
    }
    

    The value of the field can be accessed via IQueryContainer.

    I tried the QueryVisitor and the DslPrettyPrintVisitor. The first one doesn't provide any useful information. It has 0 depth and it is a Query? I already know that. With the second one I can assert some additional information, like the field name (blacklistedfor) and the suffix (keyword). Not perfect to assert on the string representation, but better than nothing.

    Edit2

    @Russ Cam gave me a solution which I am really happy with. It uses a derived QueryVisitor:

    public class MatchQueryVisitor : QueryVisitor
    {
        public string Field { get; private set; }
        public string Value { get; private set; }
    
        public override void Visit(IMatchQuery query)
        {
            var inferrer = new Inferrer(new ConnectionSettings(new InMemoryConnection()));
            Field = inferrer.Field(query.Field);
            Value = query.Query;
        }
    }
    
    [Test]
    public void test()
    {
        // Arrange
        var fieldValue = "asdf";
        var elasticClient = Substitute.For<IElasticClient>();
        var sut = new Recommender(elasticClient);
    
        // Act
        var boolQuery = sut.GetBoolQuery(new RecommenderQueryFields { BlackListedFor = new List<string> { fieldValue } }, null,
            null);
    
        // Assert
        IQueryContainer qc = boolQuery.Should.First();
        var queryVisitor = new MatchQueryVisitor();
        qc.Accept(queryVisitor);
        Assert.AreEqual(fieldValue, queryVisitor.Value);
        Assert.AreEqual("blacklistedfor.keyword", queryVisitor.Field);
    }
    

    So in MatchQueryVisitor, it gets the Field and Value, which are then asserted in the test method.