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

使用RestSharp时断言失败致响应码显示为409的问题排查

问题

我用C# + RestSharp + NUnit 3编写了POST请求测试代码,接口实际返回200状态码、响应头和响应体。我先断言状态码为200/204/302,再解析响应数据做断言。但当响应数据的断言失败时,NUnit测试报告里却显示响应码为409——明明之前的状态码断言已经通过,实际接口响应也是200。

调用代码及断言逻辑

retvals = Common.restAPI(RestSharp.Method.Post, baseUrl, authEP, authType, get_cfg("CustomerApplication:procFinance:ep", new string[1] { "{app_id}" }, new string[1] { app_id }), get_cfg("CustomerApplication:procFinance:body", new string[2] { "{app_id}", "{amount}" }, new string[2] { app_id, amount.ToString() }), pid, new string[1] { "response.show_error" }, token: auth_token);

Assert.AreEqual("False", retvals[0]);

restAPI方法内部代码

/// <summary>
/// Main rest call method to execute a test.
/// </summary>
/// <param name="method">RestSharp.Method.Get, RestSharp.Method.Post, etc.</param>
/// <param name="baseUrl">The base URL of the API</param>
/// <param name="authRes">If authType is not AuthType.None, then this is used to authenticate before submitting the REST call.</param>
/// <param name="authType">Type of authentication used (New: Uses login/password and authRes to get bearer token and add it as an auth header, Old: Uses the getOldKey method and adds it as a 'x-api-key' header, Hash: Passes in an already generated bearer token and adds it as an auth header, None: No auth needed and authRes parameter is ignored)</param>
/// <param name="resource">The API endpoint under test</param>
/// <param name="body">[Optional] Body of the API endpoint to send</param>
/// <param name="pid">[Optional] PartnerID value to create requests, authType.Old authentication, etc.</param>
/// <param name="retval">[Optional] Values to search for and return from the response body and headers (searches body first and if value not found also searches headers). Can search in nested JObjects/JArrays by sending 'val1.val2' format. If no value passed, defaults to returning all headers and body via string array.</param>
/// <param name="username">[Optional] Used with authType.New for authentication</param>
/// <param name="password">[Optional] Used with authType.New for authentication</param>
/// <param name="token">[Optional] Used with authType.Hash for authentication</param>
/// <param name="status_code">[Optional] Default expected response status code is '200, 204, 302'. If you want to override this, send an alternate code</param>
/// <param name="headerNamePipeVal">[Optional] If a custom header is needed, send a string with header name and header value delimited with a '|' (myCustomHeader|myCustomHeaderValue)</param>
/// <param name="retvalAsserts">[Optional] If assertions need to be done within the RestAPI method, pass in the same size string array here compared with the retvals string array (if retvals has an array with size of 3 but only want to assert on the second item, pass in to retvalAsserts: new string[3] { "", "my value to assert", "" }).</param>
/// <returns>String array of values found from the retval parameter.</returns>
public string[] restAPI(Method method, string baseUrl, string authRes, AuthType authType, string resource, string body = "", string pid = "", string[] retval = null, string username = "", string password = "", string token = "", int status_code = 0, string headerNamePipeVal = "", string[] retvalAsserts = null)
{
    string[] retvals = null;

    //create new rest client instance
    RestClient client = new RestClient();

    //if 'New' or 'Hash' authType, this will add the appropriate bearer token header to the request
    client = GetAuthClient(client, baseUrl, resource, authType, authRes, username, password, token);

    //create the request with the resource point
    RestRequest request = new RestRequest(resource);

    if (authType == AuthType.Old)
    {
        request.AddHeader("x-api-token", GetXAPIKey(baseUrl, authRes, pid));
    }

    request.AddHeader("Accept", "*/*");

    //add a custom header
    if (headerNamePipeVal != "")
    {
        string[] head = headerNamePipeVal.Split("|");
        request.AddHeader(head[0], head[1]);
    }

    switch (method)
    {
        case Method.Post:
            //always add this header in case the request contains a jpg upload (so far, hasn't hurt any of the other post requests)
            request.AddHeader("x-content-type", "image/jpg");

            //add the post body
            request.AddParameter("text/json", body, ParameterType.RequestBody);
            break;
        case Method.Put:
            //always add this header in case the request contains a jpg upload (so far, hasn't hurt any of the other put requests)
            request.AddHeader("x-content-type", "image/jpg");

            //add the post body
            request.AddParameter("text/json", body, ParameterType.RequestBody);
            break;
    }

    TestContext.Out.WriteLine($"Request baseUrl: {baseUrl}");
    TestContext.Out.WriteLine($"Request resource: {resource}");
    TestContext.Out.WriteLine($"Request method: {method}");
    TestContext.Out.WriteLine($"Request body: {body}");
    TestContext.Out.WriteLine("");
    
    //execute the request
    RestResponse response = client.Execute(request, method);
    
    TestContext.Out.WriteLine($"Response code: {(int)response.StatusCode}");
    TestContext.Out.WriteLine($"Response code description: {response.StatusDescription}");
    TestContext.Out.WriteLine($"Response status: {response.ResponseStatus}");
    TestContext.Out.WriteLine($"Response headers: {JsonConvert.SerializeObject(response.Headers)}");
    TestContext.Out.WriteLine($"Response body: {response.Content}");
    TestContext.Out.WriteLine("");

    //throw exception if both return values and return value assertions don't have the same length
    if (retval != null && retvalAsserts != null)
    {
        if (retval.Length != retvalAsserts.Length)
        {
            throw new Exception("Return value count and return value assertion count need to match.");
        }
    }

    Assert.Multiple(() =>
    {
        //verify return code is 200, 204 or 302 (OK, No Content or Redirect) if no return code specified, otherwise assert on the one specified
        if (status_code == 0)
        {
            CollectionAssert.Contains(new[] { 200, 204, 302 }, (int)response.StatusCode);
        }
        else
        {
            Assert.AreEqual(status_code, (int)response.StatusCode);
        }

        int retvalAssertsCount = 0;

        //parse out the specified return codes
        retvals = parseResponse(response, retval);

        //if both return values and return value assertions are not null, assert on each one that does not have a null return value assertion
        if (retval != null && retvalAsserts != null)
        {
            foreach (string rv in retvals)
            {
                if (retvalAsserts[retvalAssertsCount] != "")
                {
                    Assert.AreEqual(retvalAsserts[retvalAssertsCount], rv);
                }

                retvalAssertsCount++;
            }
        }
    });

    return retvals;
}

原因分析

  1. Assert.Multiple的混合操作干扰:你在同一个Assert.Multiple块中同时执行状态码断言和响应数据解析,如果parseResponse方法内部存在修改原始response对象的逻辑(比如误改StatusCode),或者包含其他API调用返回409,NUnit会把后续操作的状态码错误关联到测试报告中。
  2. 内外断言混淆报告:你在restAPI方法外额外添加了断言,当这个外部断言失败时,NUnit的报告逻辑可能会抓取最后一次操作的状态信息,而非原始接口的200响应状态,导致展示错误的409状态码。

修复方案

方案1:拆分状态码断言与数据解析操作

将状态码断言从Assert.Multiple中独立出来,确保原始响应状态不会被后续操作干扰:

// 先单独断言状态码,与后续解析操作隔离
if (status_code == 0)
{
    CollectionAssert.Contains(new[] { 200, 204, 302 }, (int)response.StatusCode);
}
else
{
    Assert.AreEqual(status_code, (int)response.StatusCode);
}

// 再处理数据解析和其他断言
Assert.Multiple(() =>
{
    int retvalAssertsCount = 0;
    retvals = parseResponse(response, retval);

    if (retval != null && retvalAsserts != null)
    {
        foreach (string rv in retvals)
        {
            if (retvalAsserts[retvalAssertsCount] != "")
            {
                Assert.AreEqual(retvalAsserts[retvalAssertsCount], rv);
            }
            retvalAssertsCount++;
        }
    }
});

方案2:确保parseResponse不修改原始响应对象

检查parseResponse方法的实现,确认它仅读取response的内容(如Content、Headers),绝对不修改response的StatusCode等核心属性;如果方法内部有额外API调用,要将这些调用的状态与原始接口响应状态完全隔离。

方案3:统一断言位置,避免重复断言

要么将所有断言放在restAPI方法内部的retvalAsserts参数中,要么全部放在方法外,避免NUnit报告混淆:

// 调用时通过retvalAsserts传入断言预期值,不再需要外部断言
retvals = Common.restAPI(
    RestSharp.Method.Post, 
    baseUrl, 
    authEP, 
    authType, 
    get_cfg("CustomerApplication:procFinance:ep", new string[1] { "{app_id}" }, new string[1] { app_id }), 
    get_cfg("CustomerApplication:procFinance:body", new string[2] { "{app_id}", "{amount}" }, new string[2] { app_id, amount.ToString() }), 
    pid, 
    new string[1] { "response.show_error" }, 
    token: auth_token,
    retvalAsserts: new string[1] { "False" }
);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 12:50:55