Elasticsearch Nest unit tests with Moq : fail to mock concrete class - c#

I need to mock some methods and properties in Nest.IElasticClient interface that is used in my repo.
As example I need to stub the Indices.Exists() method to return an ExistsResponse having an Exists property returning true.
The problem is that the concrete class has no interface implementation, nor setter on the Exists property, and is not declared virtual neither in Nest lib:
public class ExistsResponse : ResponseBase
{
public ExistsResponse();
public bool Exists { get; }
}
public ExistsResponse Exists(Indices index, Func<IndexExistsDescriptor, IIndexExistsRequest> selector = null);
So for the mocking I tried to set the property anyway on the concrete class, but it failed with all the following methods, I have no idea on how to do ...
/* Fail with exception :
System.NotSupportedException : Unsupported expression: x => x.Exists
Non-overridable members (here: ExistsResponse.get_Exists) may not be used in setup / verification expressions.
*/
var mock1 = new Mock<ExistsResponse>();
obj.SetupGet(f => f.Exists).Returns(true);
/* Fail with exception :
System.NotSupportedException : Unsupported expression: f => f.Exists
Non-overridable members (here: ExistsResponse.get_Exists) may not be used in setup / verification expressions.
*/
var mock2 = Mock.Of<ExistsResponse>(x => x.Exists == true);
/* Fail with exception :
System.ArgumentException : Property set method not found.
*/
var mock3 = new ExistsResponse();
var property = typeof(ExistsResponse).GetProperty("Exists", BindingFlags.Public | BindingFlags.Instance);
property.SetValue(mock3, true);
/* Fail with exception :
System.NullReferenceException (setter is null)
*/
var mock4 = new ExistsResponse();
var setter = property.GetSetMethod(true);
setter.Invoke(mock4, new object[] { true });
// My Mock on the Indices.Exists method
var elasticMock = new Mock<IElasticClient>();
elasticMock
.Setup(x => x.Indices.Exists(It.IsAny<string>(), null))
.Returns(/*** my stubbed object here ***/); // <== how to stub the concrete class to return a ExistsResponse.Exists = true ?
My libs :
Nest 7.12.1
Moq 4.15.2
XUnit 2.4.1
.Net 5
Thank you for your help

It is usually not good to mock what you don't own but if you want to mock elastic client responses the best approach will be to use InMemorryConnection, you can instantiate a new elasticClient with InMemorryConnection like this:
InMemorry Elastic Client:
var connection = new InMemoryConnection();
var connectionPool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var settings = new ConnectionSettings(connectionPool, connection);
var client = new ElasticClient(settings);
Then you will need to fake the response and ask client to return it like below:
var response = new
{
took = 1,
timed_out = false,
_shards = new
{
total = 2,
successful = 2,
failed = 0
},
hits = new
{
total = new { value = 25 },
max_score = 1.0,
hits = Enumerable.Range(1, 25).Select(i => (object)new
{
_index = "project",
_type = "project",
_id = $"Project {i}",
_score = 1.0,
_source = new { name = $"Project {i}" }
}).ToArray()
}
};
var responseBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(response));
var connection = new InMemoryConnection(responseBytes, 200);
var connectionPool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var settings = new ConnectionSettings(connectionPool, connection).DefaultIndex("project");
var client = new ElasticClient(settings);
var searchResponse = client.Search<Project>(s => s.MatchAll());
You can find more information about InMemorryConnection here.
If you want to properly follow don't mock what you don't own you need to do following steps:
Wrap ElasticClient and all Request and Responses of ElasticClient in this library, then you can easily mock what you need.
You should test your wrapper class with Integration test to be sure your wrapped class work as expected.
UPDATE:
For mocking Indices.Exists you need only to send 200 OK so you only need to do this:
var connection = new InMemoryConnection(responseBytes, 200);

Related

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?
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.

SignalR testing - how to mock groups in new version of SignalR for ASP.NET Core 2

I try to write test for my Hub method, but I don't know as because there is no documentation or code examples for current (1.0.0-alpha2-final) version of SignalR. There is my code:
[Fact]
public void SaveVisitorInfoTest()
{
//Arrange
var chatHub = new ChatHub();
var mockClients = new Mock<IHubClients>();
chatHub.Clients = mockClients.Object;
dynamic groups = new ExpandoObject();
var groupName = "SomeConversation";
string actualName = null;
string expectedName = "someName";
groups.SendVisitorInfo = new Action<string, string>(n => {
actualName = n;
});
mockClients.Setup(_ => _.Group(groupName)).Returns(groups);
//Act
chatHub.Clients.Group(groupName).InvokeAsync("SendVisitorInfo", expectedName);
// Assert
Assert.Equal(expectedName, actualName);
}
Visual Studio generates next error message while running the test:
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException :
'Moq.Language.Flow.ISetup'
does not contain a definition for 'Returns'
In old versions mock clients and groups creation looks like this:
var mockClients = new Mock<IHubCallerConnectionContext<dynamic>>();
dynamic groups = new ExpandoObject();
groups.SendVisitorInfo = new Action<string, string>(n => {
actualName = n;
});
mockClients.Setup(_ => _.Group(groupName)).Returns((ExpandoObject)groups)
But I can't use IHubCallerConnectionContext now, so I tried:
var mockClients = new Mock<IHubClients>();
but i don't know how to create mock groups in this case
Sorry for my terrible english
I don't have enough rep to comment but this link was as good as I could find for testing AspNetCore.SignalR v1.0.2:
https://buildingsteps.wordpress.com/2018/06/12/testing-signalr-hubs-in-asp-net-core-2-1/
It's still pretty ugly.
What I want to be able to do is
public Task DoCallback(IHubContext<MyHub> hubContext)
{
var clients = m_hubContext.Clients as IHubClients<IMyHubClient>;
clients.Client( "one").myCallback("Hi!");
}
Then mock out like:
var hubContext = new Mock<IHubContext<MyHub>>();
hubContext.Setup( h => h.Clients )
.Returns( m_hubClients.Object );
var hubClients = new Mock<IHubClients>();
var clientCallbacks = new Mock<IMyHubClient>();
hubClients.As<IHubClients<IMyHubClient>>()
.Setup( c => c.Client( "one" ) )
.Returns( clientCallbacks.Object );
clientCallbacks.Setup( c => c.myCallback( It.IsAny<string>() ) )
.Callback( ( stringp ) =>
{
...etc...
Hopefully in a future release...

How do I get this mock to work?

I need to make changes to a legacy class with no tests so I have started by writing a test, however the mocking (using Moq) isn't working properly.
This my test
[Test]
public void CorrectlyCopiesToLightningWhenNoLocationsExist()
{
// arrange
long orgId = Id64.NewId();
var data = LightningMapLocationsHelperTestData.ForNormalCopy(orgId);
var organisation = new Group
{
GroupId = orgId,
Name = "Test Organisation",
Type = GroupType.OrganisationGroup
};
var groupRepo = new Mock<IGroupRepository>();
groupRepo.Setup(r => r.GetGroup(orgId)).Returns(organisation);
var orgRepo = Mock.Of<IOrganisationRepository>(o => o.LightningLocationsEnabledFor(orgId));
var mapLocationRepo = new Mock<IMapLocationRepository>();
mapLocationRepo.Setup(r => r.OrganisationRepository).Returns(orgRepo);
mapLocationRepo
.Setup(r => r.GetMapLocationsByGroupIds(orgId, It.IsAny<IEnumerable<long>>(), true, true))
.Returns(data.InitialDatabaseLocations);
var lightningMapLocationRepo = new Mock<ILightningMapLocationRepository>();
lightningMapLocationRepo
.Setup(r => r.LocationsById(orgId, data.InitialLightningLocations.Select(l => l.LocationId)))
.Returns(data.InitialLightningLocations);
lightningMapLocationRepo
.Setup(r => r.AddLocations(It.IsAny<List<Location>>()))
.Callback((List<Location> locations) => data.InitialLightningLocations.AddRange(locations));
var infoMessages = new List<string>();
var errorMessages = new List<string>();
var helper = new LightningMapLocationsHelper(
(string s, object[] args) => infoMessages.Add(string.Format(s, args)),
(string s, object[] args) => errorMessages.Add(string.Format(s, args)));
List<CopyFailure> copyFailures;
// act
bool success = helper.CopyLocationsForOrganisation(orgId, 10, out copyFailures);
// assert
success.ShouldBeTrue();
(errorMessages?.Count ?? 0).ShouldBe(0);
data.InitialLightningLocations.Count.ShouldBe(data.ExpectedLightningLocations.Count);
}
Inside LightningMapLocationsHelper is the following method
private Group GetValidOrganisationGroup(long groupId)
{
var organisation = (new GroupRepository()).GetGroup(groupId);
if (organisation != null && organisation.Type == GroupType.OrganisationGroup) return organisation;
LogErrorMessage("Invalid groupId: {0}. Ignoring...", groupId);
return null;
}
that when called is using an actual instance of GroupRepository rather than the groupRepo mock set up in the test, thus causing the test to fail. As GroupRepository implements IGroupRepository I expected this to work.
public class GroupRepository : IGroupRepository {…}
Perhaps I am misunderstanding how mocking works. Can someone offer some insight to help me understand why this doesn't work, and how I can fix it? Do I have to pass the mocked classes in?

ODataComplexValue Actions and Casting

So I'm new to OData and I'm using Simple.OData.Client
So my code first looked like this :
var context = new ODataClient("http://localhost:51861/API/");
var test = context.For<Account>()
.Key("00010017")
.Action("Testing")
.Set(new Entry() { { "MyTest", New Test() { Item = "Hello World"} } })
.ExecuteAsScalarAsync<Test>()
.Result;
Leads to the following error:
"The value for parameter 'MyTest' is of type 'ODataExample.Model.MyTest'. WriteValue can only write null, ODataComplexValue, ODataEnumValue and primitive types that are not Stream type."
Ok, cool, so the exception above is informative and I can work with this by doing
ODataComplexValue MyTest = new ODataComplexValue();
ODataProperty myP = new ODataProperty();
myP.Name = "Item";
myP.Value = "Hello World";
myCustomer.TypeName = "ODataExample.Model.MyTest";
myCustomer.Properties = new[] { myP };
So now if I pass myTest into the .Set I get a working call to my OData server
.Set(new Entry() { { "MyTest", MyTest )
So Obviously I can create a ODataComplexValue with something like
var tt = new ODataComplexValue()
{
TypeName = t.GetType().FullName,
Properties = s.GetType().GetProperties().Select<PropertyInfo,ODataProperty>(x => new ODataProperty { Name = x.Name, Value = x.GetValue(t) })
};
The above works great and I an just create an extension method from that to have
.Set(new Entry() { { "MyTest", New Test() { Item = "Hello World"} } }.ToOData())
However, I can't help but feel I'm missing something, if the solution is this simple, why is Simple.OData.Client not just doing this for me? Is is there already an extension method or utility I should be using to get the same above result.
Just incase its required my webapi routing is as followed :
var function = builder.EntityType<Account>().Action("Testing");
function.Namespace = "Transaction";
function.Parameter<Test>("MyTest");
function.ReturnsFromEntitySet<Test>("Test");

moq does not return what I defined in mock.Setup

I am mocking a function in c# using Moq.
public void Mapper_GetProductMappingTest()
{
//Arrange:
Mapping mapping= new Mapping()
{
code = "AB"
};
Mapper mapper = new Mapper(new MongoStore());
CaseProduct caseProduct = new CaseProduct()
{
ProductID = "ABC",
};
Mock<Mapper> mock = new Mock<Mapper>(new MongoStore());
mock.Setup(x => x.GetMapping(caseProduct)).Returns(mapping);
// why does it return the caseproduct not the mapping?!
var mapped = mock.Object.GetMapping(caseProduct);
Assert.IsTrue(mapped.code == "AB");
This Assert.IsTrue is failed and the mock.Object.GetMapping(caseProduct) returns ABC insteadof AB.
Can somebody help me with that?

Categories