How to properly mock MongoDbClient

Alv*_* RC 2 c# unit-testing mongodb asp.net-core

Context

I'm writing unit tests for an API I've been developing and I just ran into an issue while trying to UnitTest a "Context" for accessing a MongoDB Storage.

I abstracted the current interface for my context:

public interface IProjectsContext
{
    IMongoCollection<Project> Projects { get; }
}
Run Code Online (Sandbox Code Playgroud)

I'm able to successfully use this interface, together with Moq to UnitTest my Repositories.

However, when trying to UnitTest my Context's implementation I've been unable to muster a solution for mocking the inwards:

public class ProjectsContext : IProjectsContext
{
    private const string ProjectsCollectionName = "Projects";

    private readonly IDatabaseParameters _dbParams;
    private readonly MongoClient _client;
    private readonly IMongoDatabase _database;

    private IMongoCollection<Project> _projects;

    public ProjectsContext(IDatabaseParameters dbParams)
    {
        _dbParams = dbParams ?? throw new ArgumentNullException(nameof(dbParams));
        _client = new MongoClient(_dbParams.ConnectionString);
        _database = _client.GetDatabase(_dbParams.DatabaseName);
    }

    public IMongoCollection<Project> Projects
    {
        get
        {
            if (_projects is null)
                _projects = _database.GetCollection<Project>(ProjectsCollectionName);
            return _projects;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

The unit test in question is:

private readonly Fixture _fixture = new Fixture();
private readonly Mock<IDatabaseParameters> _dbParametersMock = new Mock<IDatabaseParameters>();

public ProjectsContextTests()
{

}

[Fact(DisplayName = "Create a Project Context")]
public void CreateProjectContext()
{
    // Arrange
    _dbParametersMock.Setup(m => m.ConnectionString).Returns(_fixture.Create<string>());
    _dbParametersMock.Setup(m => m.DatabaseName).Returns(_fixture.Create<string>());

    // Act
    var result = new ProjectsContext(_dbParametersMock.Object);

    // Assert
    result.Should().NotBeNull();
    result.Should().BeAssignableTo<IProjectsContext>();
    // TODO: Write a test to assert the ProjectCollection
}
Run Code Online (Sandbox Code Playgroud)

Question

The only solution I can think of is changing my ProjectsContext to have a constructor with receives, as a parameter, the IMongoDatabase which is going to be used. However, is this the only solution?

Libraries used

I'm using the following NuGets for my UnitTests and Implementation:

  • xUnit
  • Coverlet.msbuild
  • Moq
  • AutoFixture
  • FluentAssertions
  • MongoDB

Nko*_*osi 5

ProjectsContext紧紧地耦合到实施顾虑/细节(即: MongoClient,使测试它隔离困难。

IMongoDatabase 是真正的依赖项,应该显式地注入到目标类中。

参考显式依赖原则

public class ProjectsContext : IProjectsContext {
    private const string ProjectsCollectionName = "Projects";
    private readonly IMongoDatabase database;
    private IMongoCollection<Project> projects;

    public ProjectsContext(IMongoDatabase database) {
        this.database = database;
    }

    public IMongoCollection<Project> Projects {
        get {
            if (projects is null)
                projects = database.GetCollection<Project>(ProjectsCollectionName);
            return projects;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

至于数据库的创建/初始化,该实现细节可以移动到组合根

//...ConfigureServices

services.AddScoped<IMongoDatabase>(sp => {
    var dbParams = sp.GetRequiredService<IDatabaseParameters>();
    var client = new MongoClient(dbParams.ConnectionString);
    return client.GetDatabase(dbParams.DatabaseName);
});

//...
Run Code Online (Sandbox Code Playgroud)

现在可以独立完成目标类的测试,而不会出现来自 3rd 方实现问题的意外行为

[Fact(DisplayName = "Create a Project Context")]
public void CreateProjectContext() {
    // Arrange
    var collectionMock = Mock.Of<IMongoCollection<Project>>();
    var dbMock = new Mock<IMongoDatabase>();
    dbMock.Setup(_ => _.GetCollection<Project>(It.IsAny<string>()))
        .Returns(collectionMock);

    // Act
    var result = new ProjectsContext(dbMock.Object);

    // Assert
    result.Should().NotBeNull()
        .And.BeAssignableTo<IProjectsContext>();
    //Write a test to assert the ProjectCollection
    result.Projects.Should().Be(collectionMock);
}
Run Code Online (Sandbox Code Playgroud)

  • 该解决方案按预期工作,但是我更改了服务的注册方式。我将 IMongoClient 作为单例添加到我的 DI 集合中,然后添加 IMongoDatabase 作为消耗所需 IMongoClient 的范围,最后将我的 IProjectsContext 及其实现添加到 DI 中。 (2认同)