Dapr 端到端测试编写指南:Test App 与 Test Driver 双角色实战
【免费下载链接】daprDapr is a portable runtime for building distributed applications across cloud and edge, combining event-driven architecture with workflow orchestration.项目地址: https://gitcode.com/GitHub_Trending/da/dapr
Dapr 仓库的端到端(E2E)测试采用「测试应用 + 测试驱动」的双角色模型:测试应用(Test App)作为带 Dapr Sidecar 的 HTTP 服务暴露测试入口,测试驱动(Test Driver)以e2e构建标签的 Go 测试在 Kubernetes 集群中驱动整个测试流程。本文以 hellodapr 为样板,完整讲解如何在 Dapr 仓库中新增 Test App、编写 Test Driver、利用测试框架自动管理资源生命周期,并给出从镜像构建、本地部署调试到全量回归的完整命令链。
E2E 测试架构:两个核心角色如何协作
在动手写代码之前,先理解 Dapr E2E 测试的两个基本构成,这是 writing-e2e-test.md 定义的骨架:
- Test app(测试应用):一个监听 3000 端口的简单 HTTP 服务器,暴露测试入口端点
/tests/{test},用于调用 Dapr Runtime API。测试驱动会将部署到 Kubernetes 集群并注入 Dapr Sidecar。 - Test driver(测试驱动):带有
e2e构建标签的 Go 测试文件,负责把测试应用部署到 Kubernetes 集群,再通过调用测试应用的入口端点来执行 Go 测试。
两者通过 HTTP 协作:Test Driver 只负责「编排」(部署、探活、发起请求、断言),真正的业务验证逻辑(如调用 Dapr 状态、Pub/Sub、Actor API)放在 Test App 的/tests/{test}端点中。这样设计的好处是测试应用可以被多个 Test Driver 复用,且与运行环境解耦。
前置要求:动手编写前,请先确保本地已具备 E2E 测试运行环境,即集群中已部署 Dapr 控制平面,并且能够运行现有 E2E 测试。
第一步:新增 Test App(测试应用)
Test App 位于 tests/apps/ 目录,可以被多个 Test Driver 共享。技术上可以用任意语言编写,但仓库推荐使用 Go,以保持代码库的一致与简洁。hellodapr 就是最合适的样板代码。
创建步骤
- 在 tests/apps/ 下新建测试应用目录;
- 从 tests/apps/hellodapr/ 复制样板文件(注意:当前仓库中 Dockerfile 统一放在 tests/Dockerfile,hellodapr 目录内保留
app.go与service.yaml两个文件):app.go:用 Go 编写的简单 HTTP 服务器;service.yaml:供 kubectl 使用的测试应用部署清单,仅用于开发调试,实际 E2E 测试不会使用它部署;
- 修改
app.go,加入你自己的测试逻辑; - 本地运行 Go 应用,验证
app.go暴露的 HTTP 端点。
深入样板实现:hellodapr 的 app.go
来看 tests/apps/hellodapr/app.go 的关键结构。应用默认监听3000端口,可通过环境变量PORT覆盖(init()中读取并解析):
var appPort = 3000 func init() { p := os.Getenv("PORT") if p != "" && p != "0" { appPort, _ = strconv.Atoi(p) } }路由注册在appRouter()中,共两个端点:
router.HandleFunc("/", indexHandler).Methods("GET") router.HandleFunc("/tests/{test}", testHandler).Methods("POST")GET /:健康检查入口,返回{"message":"OK"};POST /tests/{test}:测试入口,{test}路径参数是测试命令名,请求体携带 JSON(如{"message":"Hello Dapr."})。
testHandler是核心分发器,它从路径变量取出测试命令,按switch分发到具体的测试函数,并在响应中记录start_time/end_time(Unix 毫秒时间戳),便于测试端做耗时断言:
func testHandler(w http.ResponseWriter, r *http.Request) { testCommand := mux.Vars(r)["test"] var commandBody testCommandRequest err := json.NewDecoder(r.Body).Decode(&commandBody) if err != nil { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(appResponse{Message: err.Error()}) return } res := appResponse{Message: testCommand + " is not supported"} statusCode := http.StatusBadRequest startTime := epoch() switch testCommand { case "blue": statusCode, res = blueTest(commandBody) case "green": statusCode, res = greenTest(commandBody) case "envTest": statusCode, res = envTest(commandBody) } res.StartTime = startTime res.EndTime = epoch() w.WriteHeader(statusCode) json.NewEncoder(w).Encode(res) }其中envTest演示了如何读取 Sidecar 注入的环境变量DAPR_HTTP_PORT与DAPR_GRPC_PORT,可用于验证 Dapr Sidecar 是否正确注入、端口是否就绪——这是验证 Sidecar 注入类用例的典型手法。
调试与验证 Test App
- 将新测试应用目录名加入 tests/dapr_tests.mk 的
E2E_TEST_APPS变量; - 通过 make 构建并推送测试镜像(需要预先配置
DAPR_TEST_REGISTRY和DAPR_TEST_TAG两个环境变量,否则 check-e2e-env 会直接报错):
# DAPR_TEST_REGISTRY 与 DAPR_TEST_TAG 必须已配置 make build-e2e-app-[新应用目录名] make push-e2e-app-[新应用目录名]对应的 make 模板定义在 tests/dapr_tests.mk 中:build-e2e-app-$(1)通过e2e build子命令生成镜像并推送到DAPR_TEST_REGISTRY,push-e2e-app-$(1)负责推送到目标仓库。所有E2E_TEST_APPS项会被$(foreach ...)自动展开为同名目标。
- 修改
service.yaml,更新 metadata 与镜像仓库/镜像名(镜像命名约定为e2e-[测试应用名],如 tests/apps/hellodapr/service.yaml 中的dapriotest/e2e-hellodapr); - 部署到集群做联调:
kubectl apply -f service.yaml- 获取外部 IP:
kubectl get svc- 用 wget / curl / Postman 验证外部端点;
- 删除测试应用:
kubectl delete -f service.yamlservice.yaml值得注意:它通过dapr.io/enabled: "true"、dapr.io/app-id、dapr.io/app-port注解声明 Sidecar 注入,同时以LoadBalancer类型的 Service 暴露 80→3000 端口。这套清单只服务于「编写 E2E 测试前」的人工联调,正式 E2E 运行完全交给 Test Driver 与测试框架。
第二步:新增 Test Driver(测试驱动)
Test Driver 是位于 tests/e2e/ 下、带e2e构建标签的普通 Go 测试。它可以部署测试应用、执行测试,并在测试结束后清理全部应用与资源。仓库提供了 Test Runner 框架来托管测试应用的生命周期,因此你无需手动管理部署与清理;框架后续还会扩展 Pod 日志收集等功能。
重要提示:任何你部署的测试应用都必须使用对当前 Test Driver 唯一的名称。自动化 E2E 运行器可能同时运行多个 Test Driver,应用名不唯一就会与其他测试应用冲突。同理,所有有状态资源(Pub/Sub topic、状态存储、Secret 等)的命名也要对当前 Test Driver 唯一。
创建步骤
- 在 tests/e2e/ 下新建测试目录;
- 基于下述骨架创建 Go 测试文件,关键约束:
// +build e2e必须放在测试代码第一行(现代 Go 版本同时支持//go:build e2e形式,实际仓库文件如 hellodapr_test.go 两种标签都会写);TestMain(m *testing.M)定义测试应用与组件清单,供TestXxx(t *testing.T)使用;- 为测试定义独立 package;
- 遵循 Go 测试最佳实践。
// +build e2e package hellodapr_e2e import ( "testing" kube "github.com/dapr/dapr/tests/platforms/kubernetes" "github.com/dapr/dapr/tests/runner" "github.com/stretchr/testify/require" ... ) // Test runner instance var tr *runner.TestRunner // Go test main entry - https://golang.org/pkg/testing/#hdr-Main func TestMain(m *testing.M) { testApps := []kube.AppDescription{ { AppName: "hellodapr", DaprEnabled: true, ImageName: "e2e-hellodapr", Replicas: 1, IngressEnabled: true, MetricsEnabled: true, }, } tr = runner.NewTestRunner("hellodapr", testApps, nil, nil) os.Exit(tr.Start(m)) } func TestHelloDapr(t *testing.T) { externalURL := tr.Platform.AcquireAppExternalURL("hellodapr") require.NotEmpty(t, externalURL, "external URL must not be empty") // Call endpoint for "hellodapr" test app resp, err := httpGet(externalURL) require.NoError(t, err) ... } func TestStateStore(t *testing.T) { ... }TestMain:声明应用清单并启动 Test Runner
TestMain()中定义整个测试文件会用到的应用数组,然后创建 Test Runner 实例并启动:
// The array of test apps which will be used for the entire tests // defined in this test driver file testApps := []kube.AppDescription{ { AppName: "hellodapr", // app name DaprEnabled: true, // dapr sidecar injection ImageName: "e2e-hellodapr", // docker image name 'e2e-[test app name]' Replicas: 1, // number of replicas IngressEnabled: true, // enable ingress endpoint MetricsEnabled: true, // enable metrics endpoint }, } // Create test runner instance with 'hellodapr' runner id tr = runner.NewTestRunner("hellodapr", testApps, nil, nil) // Start the test os.Exit(tr.Start(m))kube.AppDescription的完整字段定义在 tests/platforms/kubernetes/app_description.go,除文档示例中的基础字段外,仓库还支持以下常用配置:
| 字段 | 类型 | 含义 |
|---|---|---|
AppName | string | 应用名(测试内唯一) |
AppPort | int | 应用监听端口,默认 3000 |
DaprEnabled | bool | 是否注入 Dapr Sidecar |
ImageName | string | 镜像名,约定e2e-[应用名] |
SidecarImage | string | 指定 Sidecar 镜像(用于版本偏斜测试) |
RegistryName/ImageSecret | string | 私有仓库与拉取凭据 |
Replicas | int32 | 副本数 |
IngressEnabled | bool | 是否暴露 LoadBalancer/NodePort 外部端点 |
IngressPort | int | 外部端口,缺省用 AppPort |
MetricsEnabled | bool | 控制dapr.io/enable-metrics注解 |
Config | string | 关联的 Dapr Configuration CRD |
AppCPULimit/AppCPURequest/AppMemoryLimit/AppMemoryRequest | string | 应用容器资源限制与请求 |
DaprCPULimit/DaprCPURequest/DaprMemoryLimit/DaprMemoryRequest | string | Sidecar 容器资源限制与请求 |
AppEnv | map[string]string | 注入到应用的环境变量 |
IsJob | bool | 是否以 Job 形式部署 |
EnableAppHealthCheck/AppHealthCheckPath等 | bool/int/string | 应用健康检查配置 |
从源码看,IngressEnabled与外部访问还受环境变量TEST_E2E_USE_INTERNAL_IP影响(见ShouldBeExposed()),集群内联调时可走 Service 内部 IP。
runner.NewTestRunner(id, apps, comps, initApps)的四个参数分别为:Runner ID(用于日志标识)、测试应用数组、Dapr 组件(ComponentDescription)数组、初始化应用数组(先于测试应用部署,可用于预置组件或执行准备工作)。
TestXxx:编写具体用例
TestXxx(t *testing.T)的通用范式是:先获取应用外部 URL,再向其入口端点发送 HTTP 请求:
func TestHelloDapr(t *testing.T) { // Acquire app external url externalURL := tr.Platform.AcquireAppExternalURL("hellodapr") require.NotEmpty(t, externalURL, "external URL must not be empty") // Call endpoint for "hellodapr" test app resp, err := httpGet(externalURL) require.NoError(t, err) ... }参考仓库真实实现 tests/e2e/hellodapr/hellodapr_test.go,一个完整的用例会覆盖「探活 → 触发测试 → 断言响应」全流程。它使用 tests/e2e/utils 提供的 HTTP 工具函数(utils.HTTPGetNTimes按numHealthChecks次轮询探活、utils.HTTPPost发送 JSON 请求体):
func TestHelloDapr(t *testing.T) { helloAppTests := []struct { testName string app string testCommand string expectedResponse string condition bool }{ {"green dapr", "hellogreendapr", "green", "Hello green dapr!", true}, {"blue dapr", "hellobluedapr", "blue", "Hello blue dapr!", true}, {"envTest dapr", "helloenvtestdapr", "envTest", "3500 50001", true}, } for _, tt := range helloAppTests { t.Run(tt.testName, func(t *testing.T) { if !tt.condition { t.Skip("Skipped because condition is false") } // Get the ingress external url of test app externalURL := tr.Platform.AcquireAppExternalURL(tt.app) require.NotEmpty(t, externalURL, "external URL must not be empty") // Check if test app endpoint is available _, err := utils.HTTPGetNTimes(externalURL, numHealthChecks) require.NoError(t, err) // Trigger test body, err := json.Marshal(testCommandRequest{Message: "Hello Dapr."}) require.NoError(t, err) resp, err := utils.HTTPPost(fmt.Sprintf("%s/tests/%s", externalURL, tt.testCommand), body) require.NoError(t, err) var appResp appResponse err = json.Unmarshal(resp, &appResp) require.NoError(t, err) require.Equal(t, tt.expectedResponse, appResp.Message) }) } }同一文件中还演示了平台操作类用例,如tr.Platform.Scale("hellobluedapr", 3)扩容副本、tr.Platform.Restart("hellobluedapr")重启 Pod,可用于验证运行时对扩缩容与重启的响应。
Test Runner 内部:应用生命周期如何被托管
TestRunner.Start() 是生命周期管理的核心,其执行顺序清晰可循:
tr.Platform.Setup()初始化测试平台(如校验 kubeconfig、准备命名空间);AddSecrets()创建测试所需的 Kubernetes Secret;AddComponents()安装 Dapr 组件;AddApps(initApps)部署初始化应用;AddApps(testApps)部署正式测试应用(含 Sidecar 注入与就绪等待,等待逻辑位于 tests/platforms/kubernetes/appmanager.go,轮询间隔 1 秒、超时 8 分钟);m.Run()执行所有TestXxx用例;defer触发的TearDown()统一清理全部资源。
这就是文档所说「不需要自己管理测试应用」的实现依据:应用部署、Sidecar 探测、资源回收都被框架接管。PlatformInterface还提供GetAppUsage、GetSidecarUsage、GetTotalRestarts、PortForwardToApp等能力,可支撑资源观测与端口转发类用例。
第三步:调试与验证 Test Driver
- 如果开发环境使用 minikube,请设置
DAPR_TEST_MINIKUBE_IP环境变量为minikube ip的输出(对应源码常量定义于 tests/platforms/kubernetes/appmanager.go); - 调试 Go 测试有两种推荐方式:
dlv test:例如想单独调试
TestHelloDapr:dlv test --build-flags='-tags=e2e' ./tests/e2e/... -- -test.run ^TestHelloDapr$VSCode + Go 插件(推荐):调试体验更佳;但由于插件暂不支持给构建选项加 build tag,调试期间需要临时移除测试文件中的
// +build e2e构建约束(并在调试结束后恢复);
- 运行全部 E2E 测试:
make test-e2e-all该目标定义在 tests/dapr_tests.mk:默认通过gotestsum以-p 3并行度运行全部tests/e2e/...包,并输出 JUnit/JSON 报告;其中hotreloading、scheduler、job等共享集群全局状态的包(DAPR_E2E_SERIAL_PACKAGES)会以-p 1串行方式单独跑一遍。若只想跑单个测试,可设置DAPR_E2E_TEST环境变量指定应用目录,例如DAPR_E2E_TEST=hellodapr make test-e2e-all。
小结与延伸阅读
回顾整个编写流程:先在 tests/apps/ 下以 hellodapr 为样板实现带/tests/{test}入口的测试应用,通过 make 目标构建推送镜像并用service.yaml人工联调;再在 tests/e2e/ 下编写带e2e标签的 Test Driver,通过AppDescription声明应用、TestRunner托管生命周期,最后用dlv或 VSCode 调试并跑通make test-e2e-all。这套双角色模式让「应用行为验证」与「集群编排」彻底解耦,也是 Dapr 全仓库 40+ 个 E2E 测试包得以并行稳定运行的基础。
想进一步深入,可继续阅读仓库中的相关文档与代码:
- E2E 测试环境搭建:本地环境与镜像仓库配置
- E2E 测试基础设施:CI 中如何运行 E2E
- 集成测试编写指南:另一层级的测试方法论
- Test Runner 实现:生命周期编排源码
- AppDescription 定义:应用描述字段全集
- 测试 Makefile:
E2E_TEST_APPS与全部 e2e 目标
【免费下载链接】daprDapr is a portable runtime for building distributed applications across cloud and edge, combining event-driven architecture with workflow orchestration.项目地址: https://gitcode.com/GitHub_Trending/da/dapr
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考