使用Moq与XUnit测试DocumentDBRepository时遇空引用异常及Mock问题
解决DocumentDBRepository单元测试的空引用异常与Mock IDocumentQuery问题
看起来你在给DocumentDBRepository写单元测试时遇到了两个头疼的问题:空引用异常和Linq场景下Mock IDocumentQuery的难题,我来一步步帮你解决!
一、先搞定空引用异常的根源
看你贴的代码,DocumentDBRepository<T>里的client是静态私有字段,但构造函数接收了IDocumentClient参数却没给这个静态字段赋值?这大概率是空引用的罪魁祸首!
静态字段属于类本身,不是实例级别的,如果构造函数里没写client = documentClient;,后续调用任何依赖client的方法时,必然触发空引用异常。而且静态字段还会导致多实例共享同一个客户端,在单元测试场景下很容易出现状态污染,更建议改成实例字段:
// 把静态字段改成只读实例字段,避免状态污染 private readonly IDocumentClient client; private readonly string DatabaseId; private readonly string CollectionId; public DocumentDBRepository(IDocumentClient documentClient, string databaseId, string collectionId) { // 加空值校验,提前暴露问题 client = documentClient ?? throw new ArgumentNullException(nameof(documentClient)); DatabaseId = databaseId ?? throw new ArgumentNullException(nameof(databaseId)); CollectionId = collectionId ?? throw new ArgumentNullException(nameof(collectionId)); }
另外,在单元测试里要确保用Moq创建的IDocumentClient实例已经正确注入到Repository构造函数,别传null哦。
二、Mock IDocumentQuery处理Linq查询的单元测试方案
IDocumentQuery<T>继承了IEnumerable<T>和IAsyncEnumerable<T>,Mock的时候得同时处理同步、异步枚举逻辑,还要模拟ExecuteNextAsync方法。下面是一套可复用的解决方案:
1. 封装Mock IDocumentQuery的辅助方法
写个通用辅助方法,避免重复造轮子:
private static Mock<IDocumentQuery<T>> CreateMockDocumentQuery<T>(List<T> testData) { var mockQuery = new Mock<IDocumentQuery<T>>(); // 处理同步Linq枚举逻辑 var enumerator = testData.GetEnumerator(); mockQuery.As<IEnumerable<T>>().Setup(x => x.GetEnumerator()).Returns(enumerator); // 处理异步Linq枚举逻辑(如果用到了Async Linq) mockQuery.As<IAsyncEnumerable<T>>() .Setup(x => x.GetAsyncEnumerator(It.IsAny<CancellationToken>())) .Returns(new AsyncEnumerator<T>(enumerator)); // 模拟ExecuteNextAsync返回测试数据 mockQuery.Setup(x => x.ExecuteNextAsync<T>(It.IsAny<CancellationToken>())) .ReturnsAsync(new FeedResponse<T>(testData)); // 如果需要模拟无更多数据的场景,可加这行 // mockQuery.Setup(x => x.HasMoreResults).Returns(false); return mockQuery; } // 辅助实现异步枚举器 public class AsyncEnumerator<T> : IAsyncEnumerator<T> { private readonly IEnumerator<T> _enumerator; public AsyncEnumerator(IEnumerator<T> enumerator) => _enumerator = enumerator; public T Current => _enumerator.Current; public ValueTask DisposeAsync() { _enumerator.Dispose(); return ValueTask.CompletedTask; } public ValueTask<bool> MoveNextAsync() => ValueTask.FromResult(_enumerator.MoveNext()); }
2. 编写具体单元测试用例
比如测试Repository的GetAllAsync方法(假设该方法用Linq查询DocumentDB):
[Fact] public async Task GetAllAsync_ReturnsAllTestItems() { // Arrange var testEntities = new List<TestEntity> { new TestEntity { Id = "1", Name = "TestItem1" }, new TestEntity { Id = "2", Name = "TestItem2" } }; var mockQuery = CreateMockDocumentQuery(testEntities); var mockClient = new Mock<IDocumentClient>(); // 模拟CreateDocumentQuery返回我们的Mock查询对象 mockClient.Setup(x => x.CreateDocumentQuery<TestEntity>( It.IsAny<Uri>(), It.IsAny<FeedOptions>())) .Returns(mockQuery.Object); var repository = new DocumentDBRepository<TestEntity>( mockClient.Object, "TestDatabase", "TestCollection"); // Act var result = await repository.GetAllAsync(); // Assert Assert.Equal(2, result.Count()); Assert.Contains(result, item => item.Name == "TestItem1"); }
关键注意点
- 确保
CreateDocumentQuery的Setup匹配你Repository里实际调用的参数(比如Uri、FeedOptions),不确定的话用It.IsAny<>模糊匹配即可。 - 如果Repository里用了Linq的
Where/OrderBy等操作,Moq会自动处理,因为我们已经Mock了IEnumerable的枚举逻辑。 - 分页场景下,需要多次Setup
ExecuteNextAsync来模拟多页数据,同时控制HasMoreResults的返回值。
内容的提问来源于stack exchange,提问作者superninja
相关产品推荐
相关产品推荐

