/DB

Unit testing

Unit-test your data-access code with no database by mocking the generated context interfaces ISocigyDatabaseFactory, I{Db} and I{Table}Set.

updated 5 Jun 20261 min readv0.3.2View as Markdown

The mockable seam

Because the database context is entirely interface-based, services that depend on ISocigyDatabaseFactory<I{Db}> can be unit-tested with no database. You mock three things:

  • ISocigyDatabaseFactory<IAuthDb>: make ExecuteAsync/ExecuteTransactionAsync invoke the delegate inline,
  • IAuthDb: return mocked table sets,
  • IUserSet (etc.): return canned data and verify calls.

Example (Moq)

The service under test depends only on the interface:

public class EnrollmentService(ISocigyDatabaseFactory<IAuthDb> db)
{
    public Task<Guid> EnrollAsync(string courseName) =>
        db.ExecuteTransactionAsync(async d =>
        {
            var course = new Course { Id = Guid.NewGuid(), Name = courseName };
            await d.Courses.InsertAsync(course);
            return course.Id;
        });
}

The test needs no PostgreSQL:

[Test]
public async Task EnrollAsync_inserts_course()
{
    var courses = new Mock<ICourseSet>();
    courses.Setup(s => s.InsertAsync(It.IsAny<Course>())).ReturnsAsync(true);

    var ctx = new Mock<IAuthDb>();
    ctx.SetupGet(c => c.Courses).Returns(courses.Object);

    var factory = new Mock<ISocigyDatabaseFactory<IAuthDb>>();
    factory
        .Setup(f => f.ExecuteTransactionAsync(It.IsAny<Func<IAuthDb, Task<Guid>>>(), It.IsAny<CancellationToken>()))
        .Returns((Func<IAuthDb, Task<Guid>> work, CancellationToken _) => work(ctx.Object)); // run inline

    var sut = new EnrollmentService(factory.Object);
    var id = await sut.EnrollAsync("Intro");

    Assert.That(id, Is.Not.EqualTo(Guid.Empty));
    courses.Verify(s => s.InsertAsync(It.Is<Course>(c => c.Name == "Intro")), Times.Once);
}

The key trick is the .Returns(...) that invokes the delegate with the mocked context, so the lambda body actually runs and your InsertAsync expectations are exercised.

Integration tests

For end-to-end coverage against a real database, resolve the factory from a real ServiceCollection (with AddAuthDb() + AddAuthDbContext()) and assert on actual rows, including transaction commit and rollback. See the library's own UnitTest.DB.Tests for a working example.

NOTE
Keep IAsyncEnumerable streaming (ForEachAsync) out of pure unit tests. Terminal methods (ToListAsync, FirstOrDefaultAsync, ExistsAsync, CountAsync) are the natural mocking surface.