Testing Neo4j.Driver (4.1.1) Part 2 – Session Config
In my previous post we showed how we could test a few different areas of the Neo4j.Driver nuget package.
One area we didn’t touch was the SessionConfig
– as part of the IAsyncSession
. Now, for the most part – things are done in interfaces, which makes our lives easier – as we can Mock
them easily.
SessionConfig
is a different beast.
Let’s look at some example code:
var session = driver.AsyncSession(builder => builder .WithDatabase("movies") .WithDefaultAccessMode(AccessMode.Read));
Here we get an IAsyncSession
that is configured for Read
access, and against the movies
database.
Let’s imagine we have decided to store our data in different databases, Movies in the movies
database, Actors in the actors
database.
We can change our existing code (in the GitHub) repo to be like the code above, and all our tests would still work.
Well. Two changes:
1 – change the Mock
for the IDriver.AsyncSession
method to be:
driverMock .Setup(x => x.AsyncSession(It.IsAny<Action<SessionConfigBuilder>>())) .Returns(sessionMock.Object);
2 – Change Test2_UsesTheAsyncSession_ToGetTheMovie()
to Verify:
driverMock.Verify(x => x.AsyncSession(It.IsAny<Action<SessionConfigBuilder>>()), Times.Once);
Nothing is checking that the AsyncSession
being created is actually pointing at the correct database.
Mocking The SessionConfig
/ SessionConfigBuilder
WARNING The code we’re going to use here can break if Neo4j change the Driver code.
Approach wise – we’re going to take the same approach we took with the Mock of the Transaction Functions where we inject our own ‘Func’.
To do that, we’ll want code that looks like this:
mockDriver .Setup(x => x.AsyncSession(It.IsAny<Action<SessionConfigBuilder>>())) .Returns((Action<SessionConfigBuilder> action) => { action( /* SOMETHING !!! */ ); return mockSession.Object; });
What we need to work out, is how to create the ‘SOMETHING’ as we can’t Mock it. So, let’s look at SessionConfigBuilder
a bit more.
First – it’s sealed
– and has no default constructor, the only one that is there is the internal
Constructor that takes a SessionConfig
.
Looking a bit more, we can see that the WithDatabase
method sets the Database
property on the SessionConfig
, and a bit further down there is a method called Build()
(again internal
) which returns the internal Config
.
So, let’s have a look at SessionConfig
– public
– so we can see it, sealed
– so we can’t extend it, and it only has 1 constructor which as above is internal
.
Hmmm. OK. Positives – the SessionConfig
class is basically a holder for data, so if we could get one of those into a SessionConfigBuilder
– we could get it constructed, then, we should be able to call Build()
on our builder
and get the config to check the settings.
We have to assume Neo4j have tested that setting the database does indeed use the correct database. That is not in the scope of these tests, nor should it be.
Get you a SessionConfig
Luckily, SessionConfig
has the default constructor, so we can make an instance in one line:
var sc = FormatterServices.GetUninitializedObject(typeof(SessionConfig)) as SessionConfig;
This gives us an uninitialized version of the SessionConfig
object, which was easy!
Get you a SessionConfigBuilder
With sc
in hand, how do we construct our builder?
With the only constructor being an internal
one with a parameter, we’re going to have to go full reflection. So. Let’s get the constructor:
var ctr = typeof(SessionConfigBuilder) .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance) .Single(c => c.GetParameters().Length == 1 && c.GetParameters().Single().ParameterType == typeof(SessionConfig));
We could probably just get away with:
var ctr = typeof(SessionConfigBuilder) .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance) .Single();
But, the extra checks mean that we will hopefully be slightly more future proof, as I can see the addition of extra constructors being relatively common.
Anyhews, once we have that, we need to call our constructor:
ctr.Invoke(new object[] {sc}) as SessionConfigBuilder;
Which we can wrap into a method:
private static SessionConfigBuilder GenerateSessionConfigBuilder() { var sc = FormatterServices.GetUninitializedObject(typeof(SessionConfig)) as SessionConfig; var ctr = typeof(SessionConfigBuilder) .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance) .Single(c => c.GetParameters().Length == 1 && c.GetParameters().Single().ParameterType == typeof(SessionConfig)); return ctr.Invoke(new object[] {sc}) as SessionConfigBuilder; }
Which means our GetMocks
method can now become:
private static void GetMocks( out Mock<IDriver> driver, out Mock<IAsyncSession> session, out Mock<IAsyncTransaction> transaction, out Mock<IResultCursor> cursor, //RETURN OUT THE BUILDER out SessionConfigBuilder sessionConfigBuilder) { var transactionMock = new Mock<IAsyncTransaction>(); var sessionMock = new Mock<IAsyncSession>(); sessionMock .Setup(x => x.ReadTransactionAsync(It.IsAny<Func<IAsyncTransaction, Task<List<Movie>>>>())) .Returns((Func<IAsyncTransaction, Task<List<Movie>>> func) => { return func(transactionMock.Object); }); var cursorMock = new Mock<IResultCursor>(); transactionMock .Setup(x => x.RunAsync(It.IsAny<string>(), It.IsAny<object>())) .Returns(Task.FromResult(cursorMock.Object)); // GENERATE OUR BUILDER var builder = GenerateSessionConfigBuilder(); var driverMock = new Mock<IDriver>(); driverMock .Setup(x => x.AsyncSession(It.IsAny<Action<SessionConfigBuilder>>())) .Returns((Action<SessionConfigBuilder> action) => { action(builder); //CALL THE BUILDER return sessionMock.Object; }); driver = driverMock; session = sessionMock; transaction = transactionMock; cursor = cursorMock; sessionConfigBuilder = builder; }
Get you a Build()
method
To test our code, we need one last thing. To get the SessionConfig
from the builder, to be able to see we’ve set the correct properties with the correct values. internal
ly they use .Build()
– so let’s write our own one.
I like extension methods for this sort of thing, so first let’s work out how to do it, then make it all look nice.
How is not really any different to the constructor stuff we looked at earlier – that’s right – reflection – which has the same problems (they might change it!).
var buildMethod = typeof(SessionConfigBuilder).GetMethod("Build", BindingFlags.NonPublic | BindingFlags.Instance);
It’s internal
– so we need the NonPublic
flags, and the name "Build"
. Once we have that – we need to Invoke
that method on the SessionConfigBuilder
instance we have:
var config = buildMethod.Invoke(builder, null) as SessionConfig;
Let’s wrap that up in a nice extension method:
public static class SessionConfigBuilderExtensions { public static SessionConfig Build(this SessionConfigBuilder scb) { var buildMethod = typeof(SessionConfigBuilder).GetMethod("Build", BindingFlags.NonPublic | BindingFlags.Instance); return buildMethod?.Invoke(scb, null) as SessionConfig; } }
OK, and let’s now finish our test:
[Fact] public async Task Part2_Test1_UsesTheCorrectDatabase() { const string expectedDb = "movies"; GetMocks(out var mockDriver, out _, out _, out _, out var builder); var movieStore = new MovieStore(mockDriver.Object); await movieStore.GetMovie_Part2("Valid"); var config = builder.Build(); config.Database.Should().Be(expectedDb); }
Now we can test that the database we expect is what we actually pass in. This is extra useful when we’re doing this against a method where we take in the database, as it means we can make sure our code is actually passing that parameter onto the session properly.
[…] Skardon takes us through some of the techniques that he used to test […]