3.31请求与响应断言
在开发或编写单元测试、集成测试时,我们经常需要验证请求内容和响应结果是否符合预期,这一过程通常称为“断言”。断言分为两类:
启用断言功能#
在开发或编写单元测试、集成测试时,我们经常需要验证请求内容和响应结果是否符合预期,这一过程通常称为“断言”。断言分为两类:
- 请求断言:在构建完
HttpRequestMessage后、发送前执行,用于校验请求的URI、方法、标头、主体等。若失败,请求不会发出。 - 响应断言:在收到
HttpResponseMessage后执行,用于校验状态码、响应头、响应体、耗时等。
若断言失败,系统会抛出 HttpAssertionException 异常。
要启用断言功能,需调用 UseAssertions() 方法,并配合 Asserts(configure) 方法使用:
HttpRequestBuilder.Get("https://furion.net") .UseAssertions();HttpRequestBuilder.Get("https://furion.net") .UseAssertions(false); // 禁用断言功能配置断言逻辑#
启用断言功能后,可通过 Asserts(configure) 方法统一配置请求断言与响应断言:
HttpRequestBuilder.Get("https://furion.net") .UseAssertions() .Asserts(ast => ast .RequestMethod(HttpMethod.Get) // 请求断言 .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 类型:
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}'."); } }); }}其中,context 参数类型为 HttpAssertionContext,包含以下属性和方法:
-
属性:
RequestMessage:发送的请求消息(HttpRequestMessage?),在请求断言阶段可用ResponseMessage:响应消息(HttpResponseMessage?),在响应断言阶段可用StatusCode:响应状态码(HttpStatusCode类型)IsSuccessStatusCode:是否请求成功(bool类型)RequestDuration:请求耗时(毫秒,long类型)ServiceProvider:服务提供器(IServiceProvider类型)
-
方法:
ReadResponseAsStringAsync():读取响应内容字符串(自动缓存,可多次读取)ReadRequestAsStringAsync():读取请求内容字符串(自动缓存,可多次读取)
使用自定义方法示例:
HttpRequestBuilder.Get("https://furion.net") .UseAssertions() .Asserts(ast => ast.ResponseIsJson().ResponseStatusCode(200)); // 支持链式调用借助 C# 扩展方法,你可以灵活扩展 HttpAssertionBuilder 的功能,提升代码的可维护性和复用性。