Unit Testing When SUT Has Static Methods

I have a simple repository InvoiceRepository which is my SUT (software under test), its using Dapper extension methods and I want to unit test the repository methods and mock out the database calls. As Im not using Entity Frameworkβ€˜s UseInMemoryDatabase so I need to mock the connection which dapper extends. Additionally I have a factory that returns the connection because thats how the cool kids do it.

See Minimal API example for a functional example of UseInMemoryDatabase.

So the code looks like this, ooh fancy its got a primary contructor, must be nice to work on modern code πŸ€“

1
2
3
4
5
6
7
public class InvoiceRepository(ISqlConnectionFactory factory) : IInvoiceRepository
{
public async Task<MyInvoice> GetAsync(Guid id)
{
using var connection = factory.CreateConnection();
return await connection
.QueryFirstAsync<MyInvoice>("SELECT * FROM dbo.my_invoice WHERE id = @id", new { id });

Note that CreateConnection returns IDbConnection and QueryFirstAsync is the dapper extension method.

Being OG noob you might be tempted to try mock the dapper method QueryFirstAsync. The code below will compile but at test runtime Moq will protest as its Guard.IsOverridable -> Guard.cs will throw with Extension methods (here: SqlMapper.QueryFirstAsync) may not be used in setup / verification expressions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using Dapper;
...
[Fact]
public async Task GetAsync_GivenWhenThen()
{
// Arrange
var factoryMock = new Mock<ISqlConnectionFactory>();
var connectionMock = new Mock<IDbConnection>();
connectionMock
.Setup(x => x.QueryFirstAsync<MyInvoice>( // static method you cannot mock, you dont deserve any snick snacks!
It.IsAny<string>(),
It.IsAny<object>(),
It.IsAny<IDbTransaction>(),
It.IsAny<int>(),
It.IsAny<CommandType>()))
.ReturnsAsync(new MyInvoice());

factoryMock
.Setup(x => x.CreateConnection())
.Returns(connectionMock.Object);
...

So we need a work around.

CAVEAT: You could argue that unit tests in the repository are low value and this is more of an integration test concearn, WebApplicationFactory and Test Containers is your friend if you go down this route, if you are a component tests kind of nerd then Component Tests With Collection & Class Fixtures is probably your jam!

On the other hand it would be nice to know that your software behaves as expected at a unit test level, things like the command text and parameters passed have not changed. The key is to be pragmatic and weigh up the options with your team and companys suggested way of working.

I’ll roll with some work-arounds, else whats the point of this blog post besides to brag about my cool primary contructor!

Workaround 1 : Wrapper

An acceptable approach is to add another layer of abstraction, you could name it based on what it does so DapperWrapper fits this description, Im childish so had a good LOL at this name πŸ˜‚πŸ€£. This abstraction would then have an interface that we can mock.

  1. Define the wrapper, here I used the same name, QueryFirstAsync for my function but you could call it something like QueryFirstWrapperAsync to mitigate confusion, or I dont know - you could read the class name.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using Dapper;
using System.Data;

public interface IDapperWrapper
{
Task<T> QueryFirstAsync<T>(IDbConnection connection, string sql, object param = null);
}

public class DapperWrapper : IDapperWrapper
{
public async Task<T> QueryFirstAsync<T>(IDbConnection connection, string sql, object param = null)
{
return await connection.QueryFirstAsync<T>(sql, param);
}
}
  1. Then inject and use the wrapper in the repository, this functions the same as before but now has the additional layer of abstraction.
1
2
3
4
5
6
7
8
9
public class InvoiceRepository(ISqlConnectionFactory factory, IDapperWrapper wrapper) : IInvoiceRepository
{
public async Task<MyInvoice> GetAsync(Guid id)
{
using var connection = factory.CreateConnection();
return await wrapper.QueryFirstAsync<MyInvoice>(
connection,
"SELECT * FROM dbo.my_invoice WHERE id = @id",
new { id });
  1. Update the test to now mock the wrapper, this will now return a new instance of MyInvoice. The tests as they stand could now just assert the response and would be adding a guard for the sql command text, honestly the integration test in the caveat above could have done that Κ•Κ˜Μ…ΝœΚ˜Μ…Κ”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[Fact]
public async Task GetAsync_GivenWhenThen()
{
// Arrange
var id = Guid.NewGuid();
var factoryMock = new Mock<ISqlConnectionFactory>();
var dapperWrapperMock = new Mock<IDapperWrapper>();
var connectionMock = new Mock<IDbConnection>();

factoryMock
.Setup(x => x.CreateConnection())
.Returns(connectionMock.Object);

dapperWrapperMock
.Setup(x => x.QueryFirstAsync<MyInvoice>(
connectionMock.Object,
"SELECT * FROM dbo.my_invoice WHERE id = @id",
It.IsAny<object>()))
.ReturnsAsync(new MyInvoice() { Id = id });

var classUnderTest = new InvoiceRepository(factoryMock.Object, dapperWrapperMock.Object);

// Act
var actual = await classUnderTest.GetAsync(Guid.NewGuid());
...
  1. I would go a step futher and now also check the param argument, as my mock setup I used It.IsAny<object>() because in the implentation the argument is new’d up, so the memory address is not the same. Remember kids, Steve Smith says new is glue.

You can use Moq.Langauge Callback Function to capture these and assert on them by value, its like magic.

Workaround 2 : delegate

Spoiler: This is pretty much the same as the wrapper but with some additional complications.

I would generally steer clear of delegates, they harder to understand and I favour code that is easy to read for my old-dad eyes, however for completness I did some delegate magic.

Potentially this code could pass the delegate as Func<T, TResult> which just defines a function that takes a parameter T and returns TResult but I thought this was was more understandable to imperative programmers like me. Also, I do what I want. I am Batman πŸ¦‡

  1. Create the dapper delegate implementation and base class. QueryFirstAsyncDeligate could be an interface but I just popped it in the base class, I dont normally code with delegates so I made πŸ’© up as I went along from here.

I just called my class file DapperDeligate.cs and popped it in the root of my Infrastructure project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using Dapper;
...
public class DapperDeligateBase
{
public delegate Task<MyInvoice> QueryFirstAsyncDeligate(
IDbConnection connection,
string sql,
object? param = null);

public required QueryFirstAsyncDeligate QueryFirstAsync { get; set; }
}

public class DapperDeligate : DapperDeligateBase
{
public DapperDeligate()
{
QueryFirstAsync = async (connection, sql, param) =>
{
return await connection
.QueryFirstAsync<MyInvoice>(sql, param);
};
}
}
  1. In the repository, inject and use the delegate
1
2
3
4
5
6
7
8
9
10
11
public class InvoiceRepository(ISqlConnectionFactory factory, DapperDeligateBase dapperDeligate) : IInvoiceRepository
{
public async Task<MyInvoice> GetAsync(Guid id)
{
using var connection = factory.CreateConnection();
return await dapperDeligate.QueryFirstAsync(
connection,
"SELECT * FROM dbo.my_invoice WHERE id = @id",
new { id });
}
}
  1. Wire up the services registration, this just means when you ask for DapperDeligateBase in your code, you will get DapperDeligate
1
2
3
builder
.Services
.AddTransient<DapperDeligateBase, DapperDeligate>();
  1. Update the tests, bet you wrote the tests first you TDD champion!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class InvoiceRepositoryTests
{
[Fact]
public async Task GetAsync_GivenWhenThen()
{
// Arrange
var expectedId = Guid.NewGuid();
var expected = new MyInvoice { Id = expectedId };
var connectionMock = new Mock<IDbConnection>();
var factoryMock = new Mock<ISqlConnectionFactory>();
var deligateMock = new Mock<DapperDeligateBase.QueryFirstAsyncDeligate>();

factoryMock
.Setup(x => x.CreateConnection())
.Returns(connectionMock.Object);

deligateMock
.Setup(x => x.Invoke(
It.IsAny<IDbConnection>(),
It.IsAny<string>(), // could pass actial command text here
It.IsAny<object>()))
.ReturnsAsync(expected);

var dapperDeligate = new DapperDeligate
{
QueryFirstAsync = deligateMock.Object
};

var classUnderTest = new InvoiceRepository(mockConnectionFactory.Object, dapperDeligate);

// Act
var actual = await classUnderTest.GetAsync(expectedId);

// Assert
Assert.NotNull(actual);
Assert.Equal(expectedId, actual.Id);
mockConnectionFactory
.Verify(x => x.CreateConnection(), Times.Once);
}
}

Then as with the wrapper, I would also use Moq.Langauge Callback Function to capture the IDbConnection, string command text and object parameters being passed to QueryFirstAsyncDeligate to validate their shape.

After writing this code I feel more like Robin 🐦