Search code examples
c#unit-testingelasticsearchnest

How to Mock Elasticsearch NEST's IGetMappingResponse for Unit Testing


Myself and my colleges have built a data access module which does CRUD and search operations to a ElasticeSearch(6.2.0) NoSQL database. We are using NEST(6.2.0), a high level client to map to the Elasticsearch query DSL. The module has been coded in C#, in a class library in a targeted framework .NET Standard 2.0.

In some methods it is necessary to retrieve the index mapping of indices stored in ElasticSearch so that field information such as field types and text properties can be used to build search queries to the Elasticsearch database. We are able to do this by using Nest’s GetMapping method (returns Nest.IGetMappingResponse).

We are not having issues executing code. Everything works fine. However, when we go to build a Unit Test for methods that uses the GetMapping method, we are not able to mock up the response it returns (IGetMappingResponse). I originally used FakeItEasy to fake it, and then inserted some of the data I needed in the object. However, to explain my issue a little better, I have created a class that implements IGetMappingResponse to mock my response. The problem comes when I try to create an instance of TypeMappings for the Mapping property of IndexMapping (In return is for property Indices for the mock). I Get an error saying “’TypMappings’ does not contain a constructor that takes 0 arguments.

public class MockedGetMappingResponse : IGetMappingResponse
{
    public IReadOnlyDictionary<IndexName, IndexMappings> Indices
    {
        get
        {
            return new ReadOnlyDictionary<Nest.IndexName, Nest.IndexMappings>(new Dictionary<Nest.IndexName, Nest.IndexMappings>
                {
                    ["statuses"] = new IndexMappings
                    {
                        Mappings = new TypeMappings() //Error happens here!!
                    }                      

                });
        }
        set { }
    }

    public IReadOnlyDictionary<IndexName, IndexMappings> Mappings
    {
        get { return null; }
        set { }
    }

    public void Accept(IMappingVisitor visitor)
    {
        // Just a test
    }

    public bool IsValid
    {
        get { return true; }
    }

    public ServerError ServerError
    {
        get { return null; }
    }

    public Exception OriginalException
    {
        get { return null; }
    }

    public string DebugInformation
    {
        get { return ""; }
    }

    public IApiCallDetails ApiCall
    {
        get { return null; }
        set { //do nothing }
    }

    public bool TryGetServerErrorReason(out string reason)
    {
        reason = "";
        return false;
    }
}      

When I look up the definitions for the Nest type TypeMappings I don’t see a constructor built. Therefore, I assume that it should be using the default, a constructor with no arguments. But apparently it doesn’t. I need to know how to go about mocking TypeMappings in IGetMappingResponse. If there is no way of creating an instance of TypeMappings, I need to know how to create a mocked IGetMappingResponse to my expected response so that I can test my code.


Solution

  • With Nkosi's help I found that TypeMappings has an internal constructor. Knowing that, I created an instance of TypeMappings using Reflection. Here is the code I used to create the object.

    IReadOnlyDictionary<TypeName, TypeMapping> backingDictionary = new ReadOnlyDictionary<TypeName, TypeMapping>(new Dictionary<TypeName, TypeMapping>
            {
                [typeName.Name] = typeMapping
    
            });           
    
            Type[] typeMappingsArgs = new Type[] { typeof(IConnectionConfigurationValues), typeof(IReadOnlyDictionary<TypeName, TypeMapping>) };
            object[] typeMappingsInputParams = new object[] { elasticClient.ConnectionSettings, backingDictionary };
            TypeMappings typeMappings = (TypeMappings)typeof(TypeMappings).GetConstructor(
                  BindingFlags.NonPublic | BindingFlags.Instance,
                  null, typeMappingsArgs, null).Invoke(typeMappingsInputParams);
    

    Here is the code for creating my mocked IGetMappingResponse. I used FakeItEasy to insert some information.

    private Nest.IGetMappingResponse GetFakeMappingResponse(Nest.IElasticClient elasticClient)
        {
            var fieldName = "fieldName";
            var indexName = "indexName";
            var documentTypeName = "documentTypeName";
    
            var typeMapping = new TypeMapping();
            var properties = new Properties();
            typeMapping.Properties = properties;
    
            var property = new TextProperty();
            property.Name = fieldName;            
    
            PropertyName propertyName = new PropertyName(fieldName);
    
            typeMapping.Properties.Add(propertyName, property);
    
            Type[] typeNameArgs = new Type[] { typeof(string) };
            object[] typeNameInputParams = new object[] { documentTypeName };
            TypeName typeName = (TypeName)typeof(TypeName).GetConstructor(
                  BindingFlags.NonPublic | BindingFlags.Instance,
                  null, typeNameArgs, null).Invoke(typeNameInputParams);
    
    
            IReadOnlyDictionary<TypeName, TypeMapping> backingDictionary = new ReadOnlyDictionary<TypeName, TypeMapping>(new Dictionary<TypeName, TypeMapping>
            {
                [typeName.Name] = typeMapping
    
            });           
    
            Type[] typeMappingsArgs = new Type[] { typeof(IConnectionConfigurationValues), typeof(IReadOnlyDictionary<TypeName, TypeMapping>) };
            object[] typeMappingsInputParams = new object[] { elasticClient.ConnectionSettings, backingDictionary };
            TypeMappings typeMappings = (TypeMappings)typeof(TypeMappings).GetConstructor(
                  BindingFlags.NonPublic | BindingFlags.Instance,
                  null, typeMappingsArgs, null).Invoke(typeMappingsInputParams);
    
            IndexMappings indexMappings = new IndexMappings();
            typeof(IndexMappings).GetProperty("Mappings", BindingFlags.Public | BindingFlags.Instance).SetValue(indexMappings, typeMappings);
    
            ReadOnlyDictionary<Nest.IndexName, Nest.IndexMappings> indices = new ReadOnlyDictionary<Nest.IndexName, Nest.IndexMappings>(new Dictionary<Nest.IndexName, Nest.IndexMappings>
            {
                [indexName] = indexMappings
    
            });
    
            var fakeMappingResponse = A.Fake<IGetMappingResponse>();
            A.CallTo(() => fakeMappingResponse.ServerError).Returns(null);
            A.CallTo(() => fakeMappingResponse.IsValid).Returns(true);
            A.CallTo(() => fakeMappingResponse.Indices).Returns(indices);
    
            return fakeMappingResponse;
        }