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

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()`。

原因分析

  1. 方法签名不匹配:实现类中调用的是泛型版本ExecuteAsync<RestResponse>(RestRequest, CancellationToken),但单元测试里Setup的是非泛型版本ExecuteAsync(RestRequest, CancellationToken),两者签名不一致,导致Moq未正确拦截调用,最终触发扩展方法内部空引用。
  2. 泛型调用冗余:ExecuteAsync<RestResponse>是用于返回序列化实体的泛型扩展方法,此处直接返回RestResponse本身,泛型参数完全多余,反而引发类型不匹配问题。
  3. 不必要的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 09:24:54