ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Tyk Gateway 测试框架完全指南:从 TestCase 到端到端 HTTP 测试

Tyk Gateway 测试框架完全指南:从 TestCase 到端到端 HTTP 测试 API网关后端云原生【免费下载链接】tykOpen Source API and AI Gateway supporting REST, GraphQL, TCP, gRPC and MCP (Model Context Protocol)项目地址https://gitcode.com/gh_mirrors/ty/tyk点击查看免费下载Tyk 是一个开源 API 与 AI 网关支持 REST、GraphQL、TCP、gRPC 以及 MCPModel Context Protocol。要为一个如此庞大、承载着认证、限流、插件、热重载等复杂功能的网关编写可靠的测试最大的挑战在于如何在表达力、可扩展性、可重复性与性能之间取得平衡。本文以仓库根目录的 TESTING.md 为骨架深入解析 Tyk 官方测试框架的设计理念、核心 API 与完整实战用法——读完本文你将能够基于test包独立编写完全走真实 HTTP 栈的网关集成测试包括 API 加载、用户会话创建、插件 bundle 注入、Dashboard/RPC/DNS 的 mock 以及可复用的 HTTP 测试运行器。为什么需要统一的测试框架在大型项目上测试方法论常常因人而异有人写集成测试、有人写单元测试有人偏好 mock、有人坚持真实环境还有先写测试与后写测试之争。随着代码库增长团队成员会各自引入自己的测试方法和辅助函数最终同一类测试出现三四种不同写法。Tyk 的测试框架就是为了解决这一问题而诞生的其核心设计要点如下所有测试都通过完整 HTTP 栈发起请求与真实用户访问网关的方式完全一致测试定义逻辑与测试运行器test runner分离测试用例只描述发什么请求、期望什么响应执行与断言由框架统一完成提供 Dashboard、RPC、Bundler 的官方 mock无需搭建真实的外部依赖绝大多数场景都需要一个网关实例Test.Gw以便直接调用网关函数、读写配置。框架位于 test 包中同时网关侧的测试辅助实现集中在 gateway/testutil.go。新框架与传统写法的对比先看一个用新框架编写的 Basic Auth 测试一个测试函数中定义了 6 个用例全部共享同一套断言和运行逻辑func genAuthHeader(username, password string) string { toEncode : strings.Join([]string{username, password}, :) encodedPass : base64.StdEncoding.EncodeToString([]byte(toEncode)) return fmt.Sprintf(Basic %s, encodedPass) } func TestBasicAuth(t *testing.T) { ts : StartTest(nil) defer ts.Close() session : ts.testPrepareBasicAuth(false) validPassword : map[string]string{Authorization: genAuthHeader(user, password)} wrongPassword : map[string]string{Authorization: genAuthHeader(user, wrong)} wrongFormat : map[string]string{Authorization: genAuthHeader(user, password:more)} malformed : map[string]string{Authorization: not base64} ts.Run(t, []test.TestCase{ // Create base auth based key {Method: POST, Path: /tyk/keys/defaultuser, Data: session, AdminAuth: true, Code: 200}, {Method: GET, Path: /, Code: 401, BodyMatch: Authorization field missing}, {Method: GET, Path: /, Headers: validPassword, Code: 200}, {Method: GET, Path: /, Headers: wrongPassword, Code: 401}, {Method: GET, Path: /, Headers: wrongFormat, Code: 400, BodyMatch: Attempted access with malformed header, values not in basic auth format}, {Method: GET, Path: /, Headers: malformed, Code: 400, BodyMatch: Attempted access with malformed header, auth data not encoded correctly}, }...) }而传统写法往往需要手动管理链、recorder 和断言例如func TestBasicAuthWrongPassword(t *testing.T) { spec : createSpecTest(t, basicAuthDef) session : createBasicAuthSession() username : 4321 // Basic auth sessions are stored as {org-id}{username}, so we need to append it here when we create the session. spec.SessionManager.UpdateSession(default4321, session, 60) to_encode : strings.Join([]string{username, WRONGPASSTEST}, :) encodedPass : base64.StdEncoding.EncodeToString([]byte(to_encode)) recorder : httptest.NewRecorder() req : testReq(t, GET, /, nil) req.Header.Set(Authorization, fmt.Sprintf(Basic %s, encodedPass)) chain : getBasicAuthChain(spec) chain.ServeHTTP(recorder, req) if recorder.Code 200 { t.Error(Request should have failed and returned non-200 code!: \n, recorder.Code) } if recorder.Code ! 401 { t.Error(Request should have returned 401 code!: \n, recorder.Code) } if recorder.Header().Get(WWW-Authenticate) { t.Error(Request should have returned WWW-Authenticate header!: \n) } }对比可见传统方式只覆盖了 1 个测试场景且断言逻辑散落在测试函数内部而新框架用 6 个声明式用例覆盖了更多分支且每个用例都可重复执行、共享统一的断言与运行器逻辑。初始化测试服务器StartTest框架的核心思想是让测试尽可能接近真实用户。为此框架提供了编程方式启动和停止完整网关 HTTP 栈的能力对应tykTestServer对象ts : StartTest(nil) defer ts.Close()StartTest 的参数StartTest的函数签名见 gateway/testutil.go为func StartTest(genConf func(globalConf *config.Config), testConfig ...TestConfig) *Test它接收两类参数genConf一个用于覆盖默认网关配置的函数。若不需要覆盖任何配置直接传nil。例如conf : func(confi *config.Config) { confi.EventHandlers eventsConf.EventHandlers } ts : StartTest(conf) defer ts.Close()testConfig可选通过TestConfig对象配置服务器行为。源码中TestConfig的完整字段定义gateway/testutil.go如下type TestConfig struct { SeparateControlAPI bool // 将 Control API 运行在独立端口 Delay time.Duration // 每个测试用例之间添加延迟依赖时序时使用虽是坏实践但有时不可避免 HotReload bool // 模拟网关通过 SIGUSR2 重启热重载 overrideDefaults bool // 覆盖监听器默认值 CoprocessConfig config.CoProcessConfig // 协处理器coprocess配置 EnableTestDNSMock bool // 是否启用测试 DNS mock }使用示例ts : gateway.StartTest(nil, gateway.TestConfig{ SeparateControlAPI: true, // 在独立端口运行 Control API delay: 10 * time.Millisecond, // 每个用例后添加延迟 hotReload: true, // 模拟 SIGUSR2 触发的网关重启 overrideDefaults: true, // 模拟覆盖监听器默认值 SkipEmptyRedis: false, // 是否跳过 Redis 清理流程 }) defer ts.Close()说明源码中的字段名以驼峰命名如SeparateControlAPI、HotReload、CoprocessConfig、EnableTestDNSMockoverrideDefaults字段当前未导出。Test 对象包含的内容StartTest()返回的Test对象gateway/testutil.go包含URL网关可达地址testRunner用于消费和测试端点的HttpTestRunnerconfigTestConfig对象即启动测试时传入的参数Gw完整的网关实例可调用任意网关函数、读取或修改当前网关配置HttpHandlerHTTP 服务器TestServerRouter测试服务器路由*mux.Router。当创建一个新服务器时框架会完成网关初始化、在随机端口启动监听器、设置所需的全局变量等。这非常接近真实启动网关进程的流程但区别在于你可以按需启动、停止和重载它。要关闭服务器调用Test#Close方法gateway/testutil.go它会确保所有监听器被正确关闭。从源码gateway/testutil.go可以看到启动流程的细节创建带取消函数的 context、通过newGateway构建网关、设置端口白名单、启动服务器、设置全局变量、初始化默认组织存储等随后基于s.URL构建HTTPTestRunner其中RequestBuilder会把每个TestCase的BaseURL指向测试网关并在AdminAuth为真时自动附加管理员认证头。加载和配置 API测试框架为 API 定义提供了一个开箱即用的极简默认模板你可以通过生成函数generator function按需修改ts : gateway.StartTest(nil) defer ts.Close() ts.Gw.buildAndLoadAPI(func(spec *APISpec) { spec.UseBasicAuth true spec.UseKeylessAccess false spec.Proxy.ListenPath / spec.OrgID default })API 定义构建后会被加载进网关立即可用于测试。buildAndLoadAPI支持以下调用形式传多个生成函数变参buildAndLoadAPI(fn1, fn2, ...)可一次加载多个 API不传参数加载默认的 API 定义buildAndLoadAPI()。实际上buildAndLoadAPI是buildAPI与loadAPI两个底层函数的组合二者都返回[]*APISpec。某些场景下你可能需要先构建 API 模板再在不同测试中做小幅修改后按需加载ts : gateway.StartTest(nil) defer ts.Close() spec : buildAPI(fn) ... spec.SomeField Case1 ts.Gw.loadAPI(spec) ... spec.SomeField Case2 ts.Gw.loadAPI(spec)修改 API 版本内的变量更新 API 版本内部的变量比较棘手因为版本对象位于Versionsmap 中直接操作 map 值是不被允许的。为此框架提供了updateAPIVersion辅助函数ts : gateway.StartTest(nil) defer ts.Close() ts.Gw.updateAPIVersion(spec, v1, func(v *apidef.VersionInfo) { v.Paths.BlackList []string{/blacklist/literal, /blacklist/{id}/test} v.UseExtendedPaths false })当通过 Go 结构体更新 API 定义比较繁琐时也可以直接借助 JSON 反序列化来更新ts : gateway.StartTest(nil) defer ts.Close() ts.Gw.updateAPIVersion(spec, v1, func(v *apidef.VersionInfo) { json.Unmarshal([]byte([ { path: /ignored/literal, method_actions: {GET: {action: no_action}} }, { path: /ignored/{id}/test, method_actions: {GET: {action: no_action}} } ]), v.ExtendedPaths.Ignored) })运行测试TestCase 与断言TestCase 结构测试用例通过test包的TestCase结构定义见 test/http.go它同时描述 HTTP 请求细节与响应断言。仓库中该结构的完整字段比文档所列更丰富type TestCase struct { Host string json:,omitempty Method string json:,omitempty Path string json:,omitempty BaseURL string json:,omitempty Domain string json:,omitempty Proto string json:,omitempty // Code 是期望的 HTTP 响应状态码 Code int json:,omitempty Data interface{} json:,omitempty Headers map[string]string json:,omitempty HeadersArray map[string][]string json:,omitempty PathParams map[string]string json:,omitempty FormParams map[string]string json:,omitempty QueryParams map[string]string json:,omitempty Cookies []*http.Cookie json:,omitempty Delay time.Duration json:,omitempty BodyMatch string json:,omitempty // 正则 BodyNotMatch string json:,omitempty HeadersMatch map[string]string json:,omitempty HeadersNotMatch map[string]string json:,omitempty JSONMatch map[string]string json:,omitempty ErrorMatch string json:,omitempty BodyMatchFunc func([]byte) bool json:- BeforeFn func() json:- Client *http.Client json:- AdminAuth bool json:,omitempty ControlRequest bool json:,omitempty }例如{Method: GET, Path: /, Headers: validPassword, Code: 200}表示向/路径发起带指定 header 的 GET 请求并在请求完成后断言响应状态码为 200。BodyMatch字段支持正则表达式匹配响应体test/http.go 中通过regexp.MustCompile实现这使得对 JSON 响应等文本做灵活断言成为可能。运行器Run 与 RunExTest提供测试运行器它根据用例规格生成 HTTP 请求并执行断言。最常用的入口是ts.Run(t, []test.TestCase{ // Create base auth based key {Method: POST, Path: /tyk/keys/defaultuser, Data: session, AdminAuth: true, Code: 200}, {Method: GET, Path: /, Code: 401, BodyMatch: Authorization field missing}, {Method: GET, Path: /, Headers: validPassword, Code: 200}, {Method: GET, Path: /, Headers: wrongPassword, Code: 401}, {Method: GET, Path: /, Headers: wrongFormat, Code: 400, BodyMatch: Attempted access with malformed header, values not in basic auth format}, {Method: GET, Path: /, Headers: malformed, Code: 400, BodyMatch: Attempted access with malformed header, auth data not encoded correctly}, }...)注意Run(t *testing.T, test.TestCase...)使用变参若需传入多个用例请像上面一样用[]test.TestCase{tc1, tc2}...加三个点的形式展开。另外还有RunEx函数当前源码中实现为RunExt见 gateway/testutil.go其签名与Run相同但内部会用overrideDefaults与hotReload的 4 种组合矩阵多次运行同一批用例组合hotReloadoverrideDefaults1falsefalse2falsetrue3truetrue4truefalse这非常适合测试与热重载功能强相关的逻辑例如 API 重载、插件 bundle 加载或监听器本身。Run与RunEx都会返回最后一个用例的响应与错误便于需要时进一步检查。修改配置变量许多测试依赖各种网关配置变量。可以通过网关配置对象直接修改ts : gateway.StartTest(nil) defer ts.Close() // 获取当前配置 currentConfig : ts.Gw.GetConfig() // 执行修改 currentConfig.HttpServerOptions.OverrideDefaults true // 应用新配置 ts.Gw.SetConfig(currentConfig) // 某些情况下需要触发重载 ts.Gw.DoReload()原文档示例中的currentConfig..HTTPProfile为笔误正确写法应为currentConfig.HttpServerOptions.OverrideDefaults true之类的配置字段。在仍使用全局配置config.Global的旧式测试中也可以这样临时修改并在测试结束后恢复config.Global.HttpServerOptions.OverrideDefaults true config.Global.HttpServerOptions.SkipURLCleaning true defer resetTestConfig()内置上游测试服务器默认创建的 API 已经指向一个为测试而构建的上游 mock其 URL 保存在testHttpAny变量中。大多数情况下你不需要直接使用它因为默认 API 已将其内嵌。默认情况下该上游 mock 会成功响应任意 URL并在响应中返回请求的详细信息格式如下type testHttpResponse struct { Method string Url string Headers map[string]string Form map[string]string }注意它返回的是最终请求的详细信息。例如要测试 URL 重写功能时原始请求的 URL 与上游 mock 响应中的 URL 会不同你可以用BodyMatch: Url:assert-url来断言。上面的 Basic Auth 测试也正是用简单的BodyMatch字符串断言来校验 JSON 响应。此外还有几个特殊 URL相关定义见 gateway/testutil.go 附近的常量/get只接受 GET 请求/post只接受 POST 请求/jwk.json用于从上游下载 JWK token 的场景对应testHttpJWK TestHttpAny /jwk.json/ws用于 WebSocket 测试/bundles内置的插件 bundle Web 服务器testHttpBundles TestHttpAny /bundles/详见下一节。Coprocess 插件测试内置 bundle 服务器要使用 Python、Lua 或 gRPC 插件通常需要 manifest 文件和脚本打包成 ZIP、上传到外部文件服务器再让网关指向 bundle 位置。Tyk 测试框架内置了 bundle 文件服务器你只需提供 bundle 文件的内容它会自动将其作为 ZIP 提供服务。流程如下创建map[string]string对象保存文件内容key 为文件名调用registerBundle(unique-plugin-id, map-with-files)返回唯一的 bundle ID创建 API 时将spec.CustomMiddlewareBundle设为registerBundle返回的 bundle ID。一个加载 Python 认证插件的完整示例var pythonBundleWithAuthCheck map[string]string{ manifest.json: { file_list: [ middleware.py ], custom_middleware: { driver: python, auth_check: { name: MyAuthHook } } } , middleware.py: from tyk.decorators import * from gateway import TykGateway as tyk Hook def MyAuthHook(request, session, metadata, spec): print(MyAuthHook is called) auth_header request.get_header(Authorization) if auth_header valid_token: session.rate 1000.0 session.per 1.0 metadata[token] valid_token return request, session, metadata , } func TestPython(t *testing.T) { ts : gateway.StartTest(nil) defer ts.Close() bundleID : ts.registerBundle(python_with_auth_check, pythonBundleWithAuthCheck) ts.Gw.buildAndLoadAPI(func(spec *APISpec) { spec.UseKeylessAccess false spec.EnableCoProcessAuth true spec.CustomMiddlewareBundle bundleID }) // test code goes here }从源码可以确认测试网关默认就启用了协处理器与 bundle 下载能力newGateway中设置了gwConfig.CoProcessOptions.EnableCoProcess true、gwConfig.EnableBundleDownloader true并把BundleBaseURL指向内置的testHttpBundlesgateway/testutil.go同时MiddlewarePath指向测试专用的临时目录。创建用户会话与创建 API 类似可以通过createSession函数创建用户会话ts : gateway.StartTest(nil) defer ts.Close() key : ts.Gw.createSession(func(s *user.SessionState) { s.QuotaMax 2 })不带参数调用createSession()则使用默认设置。如果只需要创建会话对象而不写入数据库例如需要显式通过 API 创建 key 的场景可以使用createStandardSession()函数它返回*user.SessionState对象。对应地当前源码中导出的是Test.CreateSessiongateway/testutil.go它基于CreateStandardSession()构建会话再通过POST /tyk/keys/create带AdminAuth: true写入网关最终返回创建的会话与 key 字符串。会话定义类型来自 user/session.go。自定义上游 mock如果默认上游不满足需求例如需要自定义 TLS 设置来测试 mTLS最简单的方式是使用 Go 标准库的net/http/httptest包并将 API 的spec.Proxy.TargetURL指向该测试服务器ts : gateway.StartTest(nil) defer ts.Close() upstream : httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // custom logic })) ts.Gw.buildAndLoadAPI(func(spec *APISpec) { spec.Proxy.TargetURL upstream.URL })Mocking Dashboard目前框架还没有专门的 Dashboard mock 对象但 Dashboard 本质上是标准 HTTP 服务器因此可以复用上一节自定义上游 mock的思路dashboard : httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path /system/apis { w.Write([]byte({Status: OK, Nonce: 1, Message: [{api_definition: {}}]})) } else { t.Fatal(Unknown dashboard API request, r) } })) conf : func(confi *config.Config) { confi.Global.UseDBAppConfigs true confi.Global.AllowInsecureConfigs true confi.Global.DBAppConfOptions.ConnectionString dashboard.URL } ts : gateway.StartTest(conf) defer ts.Close()这里通过genConf配置了从 Dashboard 拉取 API 定义的模式UseDBAppConfigsmock 服务器响应/system/apis端点返回的 API 定义 JSON从而在不启动真实 Dashboard 的情况下完成全流程测试。Mocking RPCHybrid 模式当网关以 Hybrid 模式运行时它通过 RPC 通道基于gorpc库与 MDCB 实例通信。可以使用startRPCMock和stopRPCMock函数来 mock RPC 服务器startRPCMock内部会自动设置启用 RPC 模式所需的配置变量相关实现见 gateway/rpc_test.gofunc TestSyncAPISpecsRPCSuccess(t *testing.T) { // Mock RPC dispatcher : gorpc.NewDispatcher() dispatcher.AddFunc(GetApiDefinitions, func(clientAddr string, dr *DefRequest) (string, error) { return [{}], nil }) dispatcher.AddFunc(Login, func(clientAddr, userKey string) bool { return true }) rpc : startRPCMock(dispatcher) defer stopRPCMock(rpc) count : syncAPISpecs() if count ! 1 { t.Error(Should return array with one spec, apiSpecs) } }DNS Mocks测试框架会覆盖默认网络解析器改用基于github.com/miekg/dns库构建的自定义 DNS 服务器 mock见 test/dns.go其中DnsMockHandle封装了 mock 服务器实例。域名到 IP 的映射定义在helpers_test.go的 map 中。默认可用域名有localhosthost1.localhost2.localhost3.local访问所有未知域名会导致 panic以便尽早暴露测试中未预期的域名访问。使用 DNS mock 意味着你可以为多个域名的 API 编写测试而无需修改机器的/etc/hosts文件。这在测试多域名 API、host 校验、域名级限流等场景中非常实用。测试网关在StartTest内部默认处理了 DNS mock 的启用gateway/testutil.go 显示EnableTestDNSMock默认为 false可通过TestConfig.EnableTestDNSMock开启。可复用的测试框架HTTPTestRunner上述测试框架的使用并不局限于 Tyk Gateway它被广泛用于 Tyk 的各个项目。其核心构件是测试运行器type HTTPTestRunner struct { Do func(*http.Request, *TestCase) (*http.Response, error) Assert func(*http.Response, *TestCase) error RequestBuilder func(*TestCase) (*http.Request, error) } func (r HTTPTestRunner) Run(t testing.TB, testCases ...TestCase) { ... }通过覆写这些变量可以定制运行器行为。例如针对外部 HTTP 服务的运行器import github.com/TykTechnologies/tyk/test ... baseURL : http://example.com runner : test.HTTPTestRunner{ Do: func(r *http.Request, tc *TestCase) (*http.Response, error) { return tc.Client.Do(r) }, RequestBuilder: func(tc *TestCase) (*http.Request, error) { tc.BaseURL baseURL return NewRequest(tc) }, } runner.Run(t, testCases...) ...也可以用于 HTTP handler 的单元测试import github.com/TykTechnologies/tyk/test ... handler : func(wr http.RequestWriter, r *http.Request){...} runner : test.HTTPTestRunner{ Do: func(r *http.Request, _ *TestCase) (*http.Response, error) { rec : httptest.NewRecorder() handler(rec, r) return rec.Result(), nil }, } runner.Run(t, testCases...) ...test包已经为上述场景导出了便捷函数实现见 test/http.gofunc TestHttpServer(t testing.TB, baseURL string, testCases ...TestCase)针对真实 HTTP 服务器func TestHttpHandler(t testing.TB, handle http.HandlerFunc, testCases ...TestCase)针对内存中的 HTTP handler。这样同一套TestCase声明式用例可以无缝地在完整网关集成测试与轻量 handler 单元测试之间复用这正是框架测试定义逻辑与测试运行器分离设计原则的直接体现。总结Tyk 测试框架的精髓可以概括为三句话测试走真实 HTTP 栈让测试结果与真实用户行为保持一致声明式 TestCase让用例既表达力强又高度可复用官方 mock 全家桶Dashboard、RPC、Bundler、DNS让你不必为基础设施分心。掌握StartTest、buildAndLoadAPI、ts.Run、registerBundle、createSession以及可复用的HTTPTestRunner你就能为 Tyk 网关的任何功能——从 Basic Auth 到插件体系、从热重载到 Hybrid 同步——编写出一致、可靠且贴近生产行为的测试。建议结合仓库中的 gateway/testutil.go、test/http.go、test/dns.go 以及各*_test.go文件继续深入研读。赞分享API网关后端云原生【免费下载链接】tykOpen Source API and AI Gateway supporting REST, GraphQL, TCP, gRPC and MCP (Model Context Protocol)项目地址https://gitcode.com/gh_mirrors/ty/tyk点击查看免费下载相关推荐ComfyUI-Inspyrenet-Rembg革命性背景移除插件超越U2Net与BRIA的终极解决方案ComfyUI Inspyrenet Rembg革命性背景移除插件超越U2Net与BRIA的终极解决方案 ComfyUI Inspyrenet Rembg是计算机视觉图像处理人工智能Duix.Avatar30分钟免费打造本地运行的专属AI数字人Duix.Avatar30分钟免费打造本地运行的专属AI数字人 Duix.Avatar 是一款免费开源的 AI 数字人离线视频生成工具。提交一段 10 秒左右人工智能AI 应用数字人媒体生成桌面应用CardSystem订单处理详解自动发货与卡密管理的最佳实践CardSystem订单处理详解自动发货与卡密管理的最佳实践 想要打造一个高效安全的卡密商城吗CardSystem作为一款专业的卡密商城系统提供了完善的订创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表