2.20HTTP 请求与响应断言(Assert)

在开发、测试中,经常需要对请求内容和响应结果进行验证,即“断言”。

在开发、测试中,经常需要对请求内容和响应结果进行验证,即“断言”。
系统将断言分为两类:

  • 请求断言:在构建完 HttpRequestMessage 后、发送前执行,用于校验请求的 URI、方法、标头、主体等。
  • 响应断言:在收到 HttpResponseMessage 后执行,用于校验状态码、响应头、响应体、耗时等。

两类断言均通过 UseAssertions() 启用,并在 Asserts(configure) 中统一配置。若断言失败,会抛出 HttpAssertionException

cs
HttpRequestBuilder.Get("https://furion.net")    .UseAssertions()    .Asserts(ast => ast        // 请求断言:发送前立即检查,失败不会发出请求        .RequestMethod(HttpMethod.Get)        .RequestUri("https://furion.net/")        // 响应断言:收到响应后检查        .ResponseStatusCode(200)        .ResponseHeaderExists("encoding")    );

其中,ast 参数为 HttpAssertionBuilder 类型,内置了以下常用断言方法(支持自定义扩展):

请求断言方法(发送前执行)#

  • RequestUri(expectedUri):断言请求 URI 等于指定字符串
    • 失败时抛出:Expected request URI to be '{expectedUri}', but found '{actual}'.
  • RequestMethod(expectedMethod):断言 HTTP 方法等于指定的 HttpMethod
    • 失败时抛出:Expected request method to be {expectedMethod}, but found {actual}.
  • RequestHeaderExists(name):断言指定的请求标头存在(包括内容标头)
    • 失败时抛出:Expected request header '{name}' to exist, but it was not found.
  • RequestHeaderEquals(name, expectedValue):断言请求标头的第一个值严格等于指定字符串(区分大小写)
    • 失败时抛出:Expected request header '{name}' to be '{expectedValue}', but found '{actual}'.
  • RequestHeaderContains(name, expectedValue):断言请求标头任意值包含指定子字符串(不区分大小写)
    • 失败时抛出:Expected request header '{name}' to contain '{expectedValue}', but the header was not found.Expected request header '{name}' to contain '{expectedValue}', but actual values were: [{...}].
  • RequestContentContains(expectedSubstring):断言请求内容包含指定子字符串(不区分大小写)
    • 失败时抛出:Expected request content to contain '{expectedSubstring}', but it was not found.
  • RequestContentEquals(expected):断言请求内容完全等于指定字符串
    • 失败时抛出:Expected request content to be '{expected}', but found '{actual}'.
  • RequestSatisfies(assertion):自定义请求断言(同步或异步),直接操作 HttpRequestMessage
    • 异步重载接受 Func<HttpRequestMessage, Task>

响应断言方法(收到响应后执行)#

  • AddAssertion(assertion):添加自定义断言委托(默认视为响应断言),如 ast.AddAssertion(async context => await ...)
  • ResponseStatusCode(statusCode):断言响应状态码等于指定值(整数或 HttpStatusCode
    • 失败时抛出:Expected response status code to be {expected}, but found {actual}.
  • ResponseStatusCodeIn(allowedStatusCodes):断言状态码在允许列表中
    • 失败时抛出:Expected response status code to be one of [{string.Join(", ", allowedStatusCodes)}], but found {actual}.
  • ResponseIsSuccessStatusCode():断言请求成功(状态码为 2xx
    • 失败时抛出:Expected response to be successful (2xx status code), but found status code {(int)context.StatusCode}.
  • ResponseContentContains(expectedSubstring):断言响应内容包含指定子字符串(不区分大小写)
    • 失败时抛出:Expected response content to contain '{expectedSubstring}', but it was not found.
  • ResponseContentEquals(expected):断言响应内容完全等于指定的字符串
    • 失败时抛出:Expected response content to be '{expected}', but found '{content}'.
  • ResponseContentMatches(pattern):断言响应内容与指定的正则表达式匹配
    • 失败时抛出:Expected response content to match regex '{pattern}', but it did not.
  • ResponseContentNotEmpty():断言响应内容不为空
    • 失败时抛出:Expected response content not to be empty.
  • ResponseHeaderExists(name):断言指定响应头存在(包括内容头)
    • 失败时抛出:Expected response header '{name}' to exist, but it was not found.
  • ResponseHeaderEquals(name, expectedValue):断言响应头的第一个值严格等于指定字符串(区分大小写)
    • 失败时抛出:Expected response header '{name}' to be '{expectedValue}', but found '{actualValue}'.
  • ResponseHeaderContains(name, expectedValue):断言响应头任意值包含指定子字符串(不区分大小写)
    • 失败时抛出:Expected response header '{name}' to contain '{expectedValue}', but the header was not found.Expected response header '{name}' to contain '{expectedValue}', but actual values were: [{string.Join(", ", values)}].
  • ResponseHeaderNotExists(name):断言指定的响应标头不存在(包括内容头)
    • 失败时抛出:Expected response header '{name}' not to exist, but it was found.
  • ResponseDurationUnder(maxMilliseconds):断言请求耗时低于指定毫秒数
    • 失败时抛出:Expected response duration to be under {maxDuration.TotalMilliseconds:F2}ms, but it took {actualDuration.TotalMilliseconds:F2}ms.
  • ResponseSatisfies(assertion):自定义响应断言(同步或异步),直接操作 HttpResponseMessage
    • 异步重载接受 Func<HttpResponseMessage, Task>

自定义断言方法#

除了内置方法,你还可以通过扩展方法为 HttpAssertionBuilder 添加自定义断言逻辑,以减少重复代码并提升可读性。例如,实现一个 ResponseIsJson 方法,用于验证响应内容是否为 application/json 类型:

cs
public static class HttpAssertionBuilderExtensions{    public static HttpAssertionBuilder ResponseIsJson(this HttpAssertionBuilder httpAssertionBuilder)    {        return httpAssertionBuilder.AddAssertion(async context =>        {            var contentType = context.ResponseMessage?.Content?.Headers.ContentType?.MediaType;            const string jsonMediaType = "application/json";            // 允许 "application/json" 或 "application/json; charset=utf-8" 等            if (string.IsNullOrEmpty(contentType) ||                !contentType.StartsWith(jsonMediaType, StringComparison.OrdinalIgnoreCase))            {                await HttpAssertionException.ThrowAsync(                    $"Expected response Content-Type to be '{jsonMediaType}' (or a subtype with parameters), but found '{contentType}'.");            }        });    }}

使用自定义方法示例:

cs
HttpRequestBuilder.Get("https://furion.net")    .UseAssertions()    .Asserts(ast => ast.ResponseIsJson().ResponseStatusCode(200));      // 支持链式调用

借助 C# 扩展方法,你可以灵活扩展 HttpAssertionBuilder 的功能,提升代码的可维护性和复用性。