Testing Neo4j.Driver (4.1.1) Part 2 – Session Config

By Charlotte
Fact

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 SessionConfigpublic – 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. internally 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.