From 271f10598f295161af2c131eb29fd0a080b161f7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:13:13 +0800 Subject: [PATCH] Add complete test scaffolding support with --test flag for API projects (#5176) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kevwan <1918356+kevwan@users.noreply.github.com> --- tools/goctl/api/gogen/gen.go | 2 + tools/goctl/api/gogen/genintegrationtest.go | 42 +++++++ tools/goctl/api/gogen/gensvctest.go | 34 ++++++ tools/goctl/api/gogen/integration_test.tpl | 120 ++++++++++++++++++++ tools/goctl/api/gogen/jwt.api | 17 +++ tools/goctl/api/gogen/svc_test.tpl | 60 ++++++++++ tools/goctl/api/gogen/template.go | 4 + 7 files changed, 279 insertions(+) create mode 100644 tools/goctl/api/gogen/genintegrationtest.go create mode 100644 tools/goctl/api/gogen/gensvctest.go create mode 100644 tools/goctl/api/gogen/integration_test.tpl create mode 100755 tools/goctl/api/gogen/jwt.api create mode 100644 tools/goctl/api/gogen/svc_test.tpl diff --git a/tools/goctl/api/gogen/gen.go b/tools/goctl/api/gogen/gen.go index f8fec6a17..b3d12642a 100644 --- a/tools/goctl/api/gogen/gen.go +++ b/tools/goctl/api/gogen/gen.go @@ -118,6 +118,8 @@ func DoGenProjectWithModule(apiFile, dir, moduleName, style string, withTest boo if withTest { logx.Must(genHandlersTest(dir, rootPkg, projectPkg, cfg, api)) logx.Must(genLogicTest(dir, rootPkg, projectPkg, cfg, api)) + logx.Must(genServiceContextTest(dir, rootPkg, projectPkg, cfg, api)) + logx.Must(genIntegrationTest(dir, rootPkg, projectPkg, cfg, api)) } if err := backupAndSweep(apiFile); err != nil { diff --git a/tools/goctl/api/gogen/genintegrationtest.go b/tools/goctl/api/gogen/genintegrationtest.go new file mode 100644 index 000000000..48c920ca1 --- /dev/null +++ b/tools/goctl/api/gogen/genintegrationtest.go @@ -0,0 +1,42 @@ +package gogen + +import ( + _ "embed" + + "github.com/zeromicro/go-zero/tools/goctl/api/spec" + "github.com/zeromicro/go-zero/tools/goctl/config" + "github.com/zeromicro/go-zero/tools/goctl/internal/version" + "github.com/zeromicro/go-zero/tools/goctl/util/format" +) + +//go:embed integration_test.tpl +var integrationTestTemplate string + +func genIntegrationTest(dir, rootPkg, projectPkg string, cfg *config.Config, api *spec.ApiSpec) error { + serviceName := api.Service.Name + if len(serviceName) == 0 { + serviceName = "server" + } + + filename, err := format.FileNamingFormat(cfg.NamingFormat, serviceName) + if err != nil { + return err + } + + return genFile(fileGenConfig{ + dir: dir, + subdir: "", + filename: filename + "_test.go", + templateName: "integrationTestTemplate", + category: category, + templateFile: integrationTestTemplateFile, + builtinTemplate: integrationTestTemplate, + data: map[string]any{ + "projectPkg": projectPkg, + "serviceName": serviceName, + "version": version.BuildVersion, + "hasRoutes": len(api.Service.Routes()) > 0, + "routes": api.Service.Routes(), + }, + }) +} diff --git a/tools/goctl/api/gogen/gensvctest.go b/tools/goctl/api/gogen/gensvctest.go new file mode 100644 index 000000000..3699ecf3e --- /dev/null +++ b/tools/goctl/api/gogen/gensvctest.go @@ -0,0 +1,34 @@ +package gogen + +import ( + _ "embed" + + "github.com/zeromicro/go-zero/tools/goctl/api/spec" + "github.com/zeromicro/go-zero/tools/goctl/config" + "github.com/zeromicro/go-zero/tools/goctl/internal/version" + "github.com/zeromicro/go-zero/tools/goctl/util/format" +) + +//go:embed svc_test.tpl +var svcTestTemplate string + +func genServiceContextTest(dir, rootPkg, projectPkg string, cfg *config.Config, api *spec.ApiSpec) error { + filename, err := format.FileNamingFormat(cfg.NamingFormat, contextFilename) + if err != nil { + return err + } + + return genFile(fileGenConfig{ + dir: dir, + subdir: contextDir, + filename: filename + "_test.go", + templateName: "svcTestTemplate", + category: category, + templateFile: svcTestTemplateFile, + builtinTemplate: svcTestTemplate, + data: map[string]any{ + "projectPkg": projectPkg, + "version": version.BuildVersion, + }, + }) +} diff --git a/tools/goctl/api/gogen/integration_test.tpl b/tools/goctl/api/gogen/integration_test.tpl new file mode 100644 index 000000000..e64c2f3e2 --- /dev/null +++ b/tools/goctl/api/gogen/integration_test.tpl @@ -0,0 +1,120 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl {{.version}} + +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "{{.projectPkg}}/internal/config" + "{{.projectPkg}}/internal/handler" + "{{.projectPkg}}/internal/svc" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zeromicro/go-zero/rest" +) + +func TestMain(m *testing.M) { + // TODO: Add setup/teardown logic here if needed + m.Run() +} + +func TestServerIntegration(t *testing.T) { + // Create test server + c := config.Config{ + RestConf: rest.RestConf{ + Host: "127.0.0.1", + Port: 0, // Use random available port + }, + } + + server := rest.MustNewServer(c.RestConf) + defer server.Stop() + + ctx := svc.NewServiceContext(c) + handler.RegisterHandlers(server, ctx) + + // Start server in background + go func() { + server.Start() + }() + + // Wait for server to start + time.Sleep(100 * time.Millisecond) + + tests := []struct { + name string + method string + path string + body string + expectedStatus int + setup func() + }{ + { + name: "health check", + method: "GET", + path: "/health", + expectedStatus: http.StatusNotFound, // Adjust based on actual routes + setup: func() {}, + }, + {{if .hasRoutes}}{{range .routes}}{ + name: "{{.Method}} {{.Path}}", + method: "{{.Method}}", + path: "{{.Path}}", + expectedStatus: http.StatusOK, // TODO: Adjust expected status + setup: func() { + // TODO: Add setup logic for this endpoint + }, + }, + {{end}}{{end}}{ + name: "not found route", + method: "GET", + path: "/nonexistent", + expectedStatus: http.StatusNotFound, + setup: func() {}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + + req, err := http.NewRequest(tt.method, tt.path, nil) + require.NoError(t, err) + + rr := httptest.NewRecorder() + server.ServeHTTP(rr, req) + + assert.Equal(t, tt.expectedStatus, rr.Code) + + // TODO: Add response body assertions + t.Logf("Response: %s", rr.Body.String()) + }) + } +} + +func TestServerLifecycle(t *testing.T) { + c := config.Config{ + RestConf: rest.RestConf{ + Host: "127.0.0.1", + Port: 0, + }, + } + + server := rest.MustNewServer(c.RestConf) + + // Test server can start and stop without errors + ctx := svc.NewServiceContext(c) + handler.RegisterHandlers(server, ctx) + + // In a real integration test, you might start the server in a goroutine + // and test actual HTTP requests, but for scaffolding we keep it simple + server.Stop() + + // TODO: Add more lifecycle tests as needed + assert.True(t, true, "Server lifecycle test passed") +} diff --git a/tools/goctl/api/gogen/jwt.api b/tools/goctl/api/gogen/jwt.api new file mode 100755 index 000000000..a8763c0d4 --- /dev/null +++ b/tools/goctl/api/gogen/jwt.api @@ -0,0 +1,17 @@ +type Request { + Name string `path:"name,options=you|me"` +} + +type Response { + Message string `json:"message"` +} + +@server( + jwt: Auth + jwtTransition: Trans + middleware: TokenValidate +) +service A-api { + @handler GreetHandler + get /greet/from/:name(Request) returns (Response) +} diff --git a/tools/goctl/api/gogen/svc_test.tpl b/tools/goctl/api/gogen/svc_test.tpl new file mode 100644 index 000000000..10009d9a4 --- /dev/null +++ b/tools/goctl/api/gogen/svc_test.tpl @@ -0,0 +1,60 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl {{.version}} + +package svc + +import ( + "testing" + + "{{.projectPkg}}/internal/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewServiceContext(t *testing.T) { + tests := []struct { + name string + config config.Config + setup func() config.Config + }{ + { + name: "default config", + setup: func() config.Config { + return config.Config{} + }, + }, + { + name: "valid config", + setup: func() config.Config { + return config.Config{ + // TODO: Add valid config values here + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := tt.setup() + svcCtx := NewServiceContext(c) + + // Basic assertions + require.NotNil(t, svcCtx) + assert.Equal(t, c, svcCtx.Config) + + // TODO: Add additional assertions for middleware and dependencies + }) + } +} + +func TestServiceContext_Initialization(t *testing.T) { + c := config.Config{} + svcCtx := NewServiceContext(c) + + // Verify service context is properly initialized + assert.NotNil(t, svcCtx) + assert.Equal(t, c, svcCtx.Config) + + // TODO: Add tests for middleware initialization if any + // TODO: Add tests for external dependencies if any +} diff --git a/tools/goctl/api/gogen/template.go b/tools/goctl/api/gogen/template.go index a00f3741c..27adc711b 100644 --- a/tools/goctl/api/gogen/template.go +++ b/tools/goctl/api/gogen/template.go @@ -22,6 +22,8 @@ const ( routesTemplateFile = "routes.tpl" routesAdditionTemplateFile = "route-addition.tpl" typesTemplateFile = "types.tpl" + svcTestTemplateFile = "svc_test.tpl" + integrationTestTemplateFile = "integration_test.tpl" ) var templates = map[string]string{ @@ -39,6 +41,8 @@ var templates = map[string]string{ routesTemplateFile: routesTemplate, routesAdditionTemplateFile: routesAdditionTemplate, typesTemplateFile: typesTemplate, + svcTestTemplateFile: svcTestTemplate, + integrationTestTemplateFile: integrationTestTemplate, } // Category returns the category of the api files.