使用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

