You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用NSubstitute模拟MongoDB Filter Definition时单元测试失败

问题

我在编写单元测试验证MongoDB仓储的Get文档方法,测试代码如下:

[Fact]
public async Task GivenExistingDocumentId_WhenGetAsync_ThenDummyProjectionReturnedSuccessfully()
{
    // Arrange
    var documentId = "123idempotentkey";
    var sourceCollection = "testcollection";

    var dummyProjectionDto = new DummyProjectionDto
    {
        IdempotencyKey = "123idempotentkey",
        Description = "test-description"
    };

    // Ensure the filter is properly initialized
    var filter = Builders<DummyProjectionDto>.Filter.Eq("_id", documentId);
    Assert.NotNull(filter);

    var cursor = Substitute.For<IAsyncCursor<DummyProjectionDto>>();
    cursor.Current.Returns(new List<DummyProjectionDto> { dummyProjectionDto });

    // Ensure filter parameter is not null when calling FindAsync
    _mongoCollectionMock.FindAsync(Arg.Is(filter), null, Arg.Any<CancellationToken>()).Returns(cursor);

    // Act
    var result = await _repository.GetAsync(documentId, sourceCollection, default);

    // Assert
    result.Should().NotBeNull();
    result.IdempotencyKey.Should().Be(dummyProjectionDto.IdempotencyKey);
    result.Description.Should().Be(dummyProjectionDto.Description);
}

运行测试时抛出以下错误:

Message:  System.ArgumentNullException : Value cannot be null.
(Parameter 'filter')

Stack Trace:  Ensure.IsNotNull[T](T value, String paramName)
IMongoCollectionExtensions.FindAsync[TDocument](IMongoCollection1 collection, FilterDefinition1 filter, FindOptions`2 options,
CancellationToken cancellationToken)
DummyProjectionRepositoryTests.GivenExistingDocumentId_WhenGetAsync_ThenDummyProjectionReturnedSuccessfully() line 98
--- End of stack trace from previous location ---

我已经确认测试代码里的filter不为空(断言通过),仓储的GetAsync实现代码如下:

public async Task<DummyProjection> GetAsync(string documentId, string sourceCollection, CancellationToken ct = default)
{
    var filter = Builders<DummyProjectionDto>.Filter.Eq("_id", documentId);

    var result = await _database
        .GetCollection<DummyProjectionDto>(GetCollectionName(sourceCollection))
        .FindAsync(filter, null, ct);

    var projectionDto = await result.SingleOrDefaultAsync(ct);

    if (projectionDto == null)
    {
        throw new MongoException($"Document with ID {documentId} not found.");
    }
    else
    {
        var projectionDocument = new DummyProjection()
        {
            IdempotencyKey = projectionDto.IdempotencyKey,
            Description = projectionDto.Description,
        };

        return projectionDocument;
    }
}

明明filter不为空,测试为何仍抛出针对'filter'的ArgumentNullException?

问题原因与解决方案
  • 核心原因:参数匹配不生效
    测试中用Arg.Is(filter)匹配FindAsync的filter参数,但FilterDefinition<T>的默认相等性判断不会认为测试里创建的filter和仓储方法内部创建的filter是同一个对象。NSubstitute找不到匹配的调用时会返回null,而MongoDB的FindAsync扩展方法会强制检查filter非空,因此抛出异常。

  • 修改参数匹配逻辑
    放弃直接匹配filter对象,改用任意参数匹配或验证filter的逻辑内容:

    // 方式1:匹配任意FilterDefinition,简单直接
    _mongoCollectionMock.FindAsync(Arg.Any<FilterDefinition<DummyProjectionDto>>(), null, Arg.Any<CancellationToken>()).Returns(cursor);
    
    // 方式2:验证filter的实际内容,更严谨
    _mongoCollectionMock.FindAsync(
        Arg.Is<FilterDefinition<DummyProjectionDto>>(f => f.ToString() == Builders<DummyProjectionDto>.Filter.Eq("_id", documentId).ToString()),
        null,
        Arg.Any<CancellationToken>()
    ).Returns(cursor);
    
  • 额外检查:确保集合模拟正确
    要确认_database.GetCollection<DummyProjectionDto>(...)返回的是你模拟的_mongoCollectionMock,如果返回未模拟的实例或null,同样会触发参数检查错误。可以在测试的Arrange阶段添加:

    _database.GetCollection<DummyProjectionDto>(Arg.Is(sourceCollection)).Returns(_mongoCollectionMock);
    

内容的提问来源于stack exchange,提问作者JsonStatham

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 10:57:28