RestSharp IRestClient模拟抛空引用异常,但实际运行正常
RestSharp异步调用Moq模拟空引用异常排查与解决
问题背景
基于.NET Framework 4.8 + RestSharp v110.2.0开发Rest API客户端,弃用旧版Execute/ExecuteTaskAsync改用ExecuteAsync。真实API调用可正常返回RestResponse,但用Moq模拟IRestClient编写单元测试时,持续抛出NullReferenceException,异常栈指向RestSharp.RestClientExtensions.<ExecuteAsync>d__101.MoveNext()`。
原因分析
- 方法签名不匹配:实现类中调用的是泛型版本
ExecuteAsync<RestResponse>(RestRequest, CancellationToken),但单元测试里Setup的是非泛型版本ExecuteAsync(RestRequest, CancellationToken),两者签名不一致,导致Moq未正确拦截调用,最终触发扩展方法内部空引用。 - 泛型调用冗余:
ExecuteAsync<RestResponse>是用于返回序列化实体的泛型扩展方法,此处直接返回RestResponse本身,泛型参数完全多余,反而引发类型不匹配问题。 - 不必要的
CallBase=true:Moq设置CallBase=true会尝试调用接口默认实现(接口无默认实现),可能触发未预期的异常。
解决方法
1. 修正实现类:改用非泛型ExecuteAsync
将MakeAuthApiCallAsync中的泛型调用改为非泛型版本,直接返回RestResponse,避免类型不匹配:
public virtual async Task<RestResponse> MakeAuthApiCallAsync() { var options = new RestClientOptions(this.Url) { MaxTimeout = -1, }; var request = new RestRequest("oauth/token", Method.Post); request.AddHeader("Content-Type", "application/x-www-form-urlencoded"); request.AddParameter("grant_type", "client_credentials"); request.AddParameter("client_id", this.Key); request.AddParameter("client_secret", this.Secret); IRestClient client = this.Client ?? new RestClient(options); // 改用非泛型ExecuteAsync,直接返回RestResponse var task = await client.ExecuteAsync(request, CancellationToken.None); return task; }
2. 修正单元测试:匹配正确签名并移除CallBase=true
调整Moq的Setup逻辑,匹配非泛型ExecuteAsync方法,同时移除CallBase=true确保完全模拟:
[Test()] public void TestMakeAuthApiCall_SupplyParameters_MatchResponseResults() { // Arrange var url = "http://test.com"; var key = "12345"; var secret = "54321"; var auth = new AuthSample() { Url = url, Key = key, Secret = secret }; var response = new RestResponse() { Content = "blah", StatusCode = HttpStatusCode.OK, // 初始化必要属性避免潜在空引用 ResponseStatus = ResponseStatus.Completed }; // 移除CallBase=true,完全模拟IRestClient行为 var restClient = new Mock<IRestClient>(); // Setup非泛型ExecuteAsync方法,匹配调用签名 restClient.Setup(x => x.ExecuteAsync(It.IsAny<RestRequest>(), It.IsAny<CancellationToken>())) .ReturnsAsync(response); auth.Client = restClient.Object; // Act var result = auth.MakeAuthApiCallAsync().GetAwaiter().GetResult(); // Assert Assert.AreEqual("blah", result.Content); Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); }
备选方案:若需保留泛型调用(不推荐)
如果因业务需求必须使用泛型ExecuteAsync,需Setup对应的泛型方法,并返回RestResponse<RestResponse>类型:
// 测试中Setup泛型版本 restClient.Setup(x => x.ExecuteAsync<RestResponse>(It.IsAny<RestRequest>(), It.IsAny<CancellationToken>())) .ReturnsAsync(new RestResponse<RestResponse> { Content = "blah", StatusCode = HttpStatusCode.OK, Data = new RestResponse() // 按需初始化Data属性 });
额外优化建议
- 避免同步阻塞异步方法:
GetResponse中使用GetAwaiter().GetResult()可能导致死锁,建议改为异步方法:public async Task<string> GetResponseAsync() { this.Url = "http://test.com"; this.Key = "12345"; this.Secret = "54321"; var response = await MakeAuthApiCallAsync(); return response.Content; } - 改用构造函数注入:将
IRestClient通过构造函数注入,替代公共字段,提升代码封装性和可测试性:public class AuthSample : IAuthSample { private readonly string _url; private readonly string _key; private readonly string _secret; private readonly IRestClient _client; public AuthSample(string url, string key, string secret, IRestClient client) { _url = url; _key = key; _secret = secret; _client = client; } // 后续方法使用私有字段而非公共字段 }
内容的提问来源于stack exchange,提问作者BBrowsk6605
相关产品推荐
相关产品推荐

