Implement #4442 , goctl generate unit test files for api handler and logic (#4443)

This commit is contained in:
Devin 2025-01-22 14:52:49 +08:00 committed by GitHub
parent bf883101d7
commit 6138f85470
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 338 additions and 6 deletions

View File

@ -72,6 +72,7 @@ func init() {
goCmdFlags.StringVar(&gogen.VarStringHome, "home")
goCmdFlags.StringVar(&gogen.VarStringRemote, "remote")
goCmdFlags.StringVar(&gogen.VarStringBranch, "branch")
goCmdFlags.BoolVar(&gogen.VarBoolWithTest, "test")
goCmdFlags.StringVarWithDefaultValue(&gogen.VarStringStyle, "style", config.DefaultFormat)
javaCmdFlags.StringVar(&javagen.VarStringDir, "dir")

View File

@ -38,7 +38,8 @@ var (
// VarStringBranch describes the branch.
VarStringBranch string
// VarStringStyle describes the style of output files.
VarStringStyle string
VarStringStyle string
VarBoolWithTest bool
)
// GoCommand gen go project files from command line
@ -49,6 +50,7 @@ func GoCommand(_ *cobra.Command, _ []string) error {
home := VarStringHome
remote := VarStringRemote
branch := VarStringBranch
withTest := VarBoolWithTest
if len(remote) > 0 {
repo, _ := util.CloneIntoGitHome(remote, branch)
if len(repo) > 0 {
@ -66,11 +68,11 @@ func GoCommand(_ *cobra.Command, _ []string) error {
return errors.New("missing -dir")
}
return DoGenProject(apiFile, dir, namingStyle)
return DoGenProject(apiFile, dir, namingStyle, withTest)
}
// DoGenProject gen go project files with api file
func DoGenProject(apiFile, dir, style string) error {
func DoGenProject(apiFile, dir, style string, withTest bool) error {
api, err := parser.Parse(apiFile)
if err != nil {
return err
@ -100,6 +102,10 @@ func DoGenProject(apiFile, dir, style string) error {
logx.Must(genHandlers(dir, rootPkg, cfg, api))
logx.Must(genLogic(dir, rootPkg, cfg, api))
logx.Must(genMiddleware(dir, cfg, api))
if withTest {
logx.Must(genHandlersTest(dir, rootPkg, cfg, api))
logx.Must(genLogicTest(dir, rootPkg, cfg, api))
}
if err := backupAndSweep(apiFile); err != nil {
return err

View File

@ -348,7 +348,7 @@ func validateWithCamel(t *testing.T, api, camel string) {
assert.Nil(t, err)
err = initMod(dir)
assert.Nil(t, err)
err = DoGenProject(api, dir, camel)
err = DoGenProject(api, dir, camel, true)
assert.Nil(t, err)
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if strings.HasSuffix(path, ".go") {

View File

@ -0,0 +1,80 @@
package gogen
import (
_ "embed"
"fmt"
"strings"
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
"github.com/zeromicro/go-zero/tools/goctl/config"
"github.com/zeromicro/go-zero/tools/goctl/util"
"github.com/zeromicro/go-zero/tools/goctl/util/format"
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
)
//go:embed handler_test.tpl
var handlerTestTemplate string
func genHandlerTest(dir, rootPkg string, cfg *config.Config, group spec.Group, route spec.Route) error {
handler := getHandlerName(route)
handlerPath := getHandlerFolderPath(group, route)
pkgName := handlerPath[strings.LastIndex(handlerPath, "/")+1:]
logicName := defaultLogicPackage
if handlerPath != handlerDir {
handler = strings.Title(handler)
logicName = pkgName
}
filename, err := format.FileNamingFormat(cfg.NamingFormat, handler)
if err != nil {
return err
}
return genFile(fileGenConfig{
dir: dir,
subdir: getHandlerFolderPath(group, route),
filename: filename + "_test.go",
templateName: "handlerTestTemplate",
category: category,
templateFile: handlerTestTemplateFile,
builtinTemplate: handlerTestTemplate,
data: map[string]any{
"PkgName": pkgName,
"ImportPackages": genHandlerTestImports(group, route, rootPkg),
"HandlerName": handler,
"RequestType": util.Title(route.RequestTypeName()),
"ResponseType": util.Title(route.ResponseTypeName()),
"LogicName": logicName,
"LogicType": strings.Title(getLogicName(route)),
"Call": strings.Title(strings.TrimSuffix(handler, "Handler")),
"HasResp": len(route.ResponseTypeName()) > 0,
"HasRequest": len(route.RequestTypeName()) > 0,
"HasDoc": len(route.JoinedDoc()) > 0,
"Doc": getDoc(route.JoinedDoc()),
},
})
}
func genHandlersTest(dir, rootPkg string, cfg *config.Config, api *spec.ApiSpec) error {
for _, group := range api.Service.Groups {
for _, route := range group.Routes {
if err := genHandlerTest(dir, rootPkg, cfg, group, route); err != nil {
return err
}
}
}
return nil
}
func genHandlerTestImports(group spec.Group, route spec.Route, parentPkg string) string {
imports := []string{
//fmt.Sprintf("\"%s\"", pathx.JoinPackages(parentPkg, getLogicFolderPath(group, route))),
fmt.Sprintf("\"%s\"", pathx.JoinPackages(parentPkg, contextDir)),
fmt.Sprintf("\"%s\"", pathx.JoinPackages(parentPkg, configDir)),
}
if len(route.RequestTypeName()) > 0 {
imports = append(imports, fmt.Sprintf("\"%s\"\n", pathx.JoinPackages(parentPkg, typesDir)))
}
return strings.Join(imports, "\n\t")
}

View File

@ -0,0 +1,90 @@
package gogen
import (
_ "embed"
"fmt"
"strings"
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
"github.com/zeromicro/go-zero/tools/goctl/config"
"github.com/zeromicro/go-zero/tools/goctl/util/format"
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
)
//go:embed logic_test.tpl
var logicTestTemplate string
func genLogicTest(dir, rootPkg string, cfg *config.Config, api *spec.ApiSpec) error {
for _, g := range api.Service.Groups {
for _, r := range g.Routes {
err := genLogicTestByRoute(dir, rootPkg, cfg, g, r)
if err != nil {
return err
}
}
}
return nil
}
func genLogicTestByRoute(dir, rootPkg string, cfg *config.Config, group spec.Group, route spec.Route) error {
logic := getLogicName(route)
goFile, err := format.FileNamingFormat(cfg.NamingFormat, logic)
if err != nil {
return err
}
imports := genLogicTestImports(route, rootPkg)
var responseString string
var returnString string
var requestString string
var requestType string
if len(route.ResponseTypeName()) > 0 {
resp := responseGoTypeName(route, typesPacket)
responseString = "(resp " + resp + ", err error)"
returnString = "return"
} else {
responseString = "error"
returnString = "return nil"
}
if len(route.RequestTypeName()) > 0 {
requestString = "req *" + requestGoTypeName(route, typesPacket)
requestType = requestGoTypeName(route, typesPacket)
}
subDir := getLogicFolderPath(group, route)
return genFile(fileGenConfig{
dir: dir,
subdir: subDir,
filename: goFile + "_test.go",
templateName: "logicTestTemplate",
category: category,
templateFile: logicTestTemplateFile,
builtinTemplate: logicTestTemplate,
data: map[string]any{
"pkgName": subDir[strings.LastIndex(subDir, "/")+1:],
"imports": imports,
"logic": strings.Title(logic),
"function": strings.Title(strings.TrimSuffix(logic, "Logic")),
"responseType": responseString,
"returnString": returnString,
"request": requestString,
"hasRequest": len(requestType) > 0,
"hasResponse": len(route.ResponseTypeName()) > 0,
"requestType": requestType,
"hasDoc": len(route.JoinedDoc()) > 0,
"doc": getDoc(route.JoinedDoc()),
},
})
}
func genLogicTestImports(route spec.Route, parentPkg string) string {
var imports []string
//imports = append(imports, `"context"`+"\n")
imports = append(imports, fmt.Sprintf("\"%s\"", pathx.JoinPackages(parentPkg, contextDir)))
imports = append(imports, fmt.Sprintf("\"%s\"", pathx.JoinPackages(parentPkg, configDir)))
if shallImportTypesPackage(route) {
imports = append(imports, fmt.Sprintf("\"%s\"\n", pathx.JoinPackages(parentPkg, typesDir)))
}
//imports = append(imports, fmt.Sprintf("\"%s/core/logx\"", vars.ProjectOpenSourceURL))
return strings.Join(imports, "\n\t")
}

View File

@ -0,0 +1,81 @@
package {{.PkgName}}
import (
"bytes"
{{if .HasRequest}}"encoding/json"{{end}}
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
{{.ImportPackages}}
)
{{if .HasDoc}}{{.Doc}}{{end}}
func Test{{.HandlerName}}(t *testing.T) {
// new service context
c := config.Config{}
svcCtx := svc.NewServiceContext(c)
// init mock service context here
tests := []struct {
name string
reqBody interface{}
wantStatus int
wantResp string
setupMocks func()
}{
{
name: "invalid request body",
reqBody: "invalid",
wantStatus: http.StatusBadRequest,
wantResp: "unsupported type", // Adjust based on actual error response
setupMocks: func() {
// No setup needed for this test case
},
},
{
name: "handler error",
{{if .HasRequest}}reqBody: types.{{.RequestType}}{
//TODO: add fields here
},
{{end}}wantStatus: http.StatusBadRequest,
wantResp: "error", // Adjust based on actual error response
setupMocks: func() {
// Mock login logic to return an error
},
},
{
name: "handler successful",
{{if .HasRequest}}reqBody: types.{{.RequestType}}{
//TODO: add fields here
},
{{end}}wantStatus: http.StatusOK,
wantResp: `{"code":0,"msg":"success","data":{}}`, // Adjust based on actual success response
setupMocks: func() {
// Mock login logic to return success
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupMocks()
var reqBody []byte
{{if .HasRequest}}var err error
reqBody, err = json.Marshal(tt.reqBody)
require.NoError(t, err){{end}}
req, err := http.NewRequest("POST", "/ut", bytes.NewBuffer(reqBody))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler := {{.HandlerName}}(svcCtx)
handler.ServeHTTP(rr, req)
t.Log(rr.Body.String())
assert.Equal(t, tt.wantStatus, rr.Code)
assert.Contains(t, rr.Body.String(), tt.wantResp)
})
}
}

View File

@ -0,0 +1,69 @@
package {{.pkgName}}
import (
"context"
"testing"
{{.imports}}
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test{{.logic}}_{{.function}}(t *testing.T) {
c := config.Config{}
mockSvcCtx := svc.NewServiceContext(c)
// init mock service context here
tests := []struct {
name string
ctx context.Context
setupMocks func()
{{if .hasRequest}}req *{{.requestType}}{{end}}
wantErr bool
checkResp func{{if .hasResponse}}{{.responseType}}{{else}}(err error){{end}}
}{
{
name: "response error",
ctx: context.Background(),
setupMocks: func() {
// mock data for this test case
},
{{if .hasRequest}}req: &{{.requestType}}{
// TODO: init your request here
},{{end}}
wantErr: true,
checkResp: func{{if .hasResponse}}{{.responseType}}{{else}}(err error){{end}} {
// TODO: Add your check logic here
},
},
{
name: "successful",
ctx: context.Background(),
setupMocks: func() {
// Mock data for this test case
},
{{if .hasRequest}}req: &{{.requestType}}{
// TODO: init your request here
},{{end}}
wantErr: false,
checkResp: func{{if .hasResponse}}{{.responseType}}{{else}}(err error){{end}} {
// TODO: Add your check logic here
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupMocks()
l := New{{.logic}}(tt.ctx, mockSvcCtx)
{{if .hasResponse}}resp, {{end}}err := l.{{.function}}({{if .hasRequest}}tt.req{{end}})
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
{{if .hasResponse}}assert.NotNil(t, resp){{end}}
}
tt.checkResp({{if .hasResponse}}resp, {{end}}err)
})
}
}

View File

@ -12,7 +12,9 @@ const (
contextTemplateFile = "context.tpl"
etcTemplateFile = "etc.tpl"
handlerTemplateFile = "handler.tpl"
handlerTestTemplateFile = "handler_test.tpl"
logicTemplateFile = "logic.tpl"
logicTestTemplateFile = "logic_test.tpl"
mainTemplateFile = "main.tpl"
middlewareImplementCodeFile = "middleware.tpl"
routesTemplateFile = "routes.tpl"
@ -25,7 +27,9 @@ var templates = map[string]string{
contextTemplateFile: contextTemplate,
etcTemplateFile: etcTemplate,
handlerTemplateFile: handlerTemplate,
handlerTestTemplateFile: handlerTestTemplate,
logicTemplateFile: logicTemplate,
logicTestTemplateFile: logicTestTemplate,
mainTemplateFile: mainTemplate,
middlewareImplementCodeFile: middlewareImplementCode,
routesTemplateFile: routesTemplate,

View File

@ -83,6 +83,6 @@ func CreateServiceCommand(_ *cobra.Command, args []string) error {
return err
}
err = gogen.DoGenProject(apiFilePath, abs, VarStringStyle)
err = gogen.DoGenProject(apiFilePath, abs, VarStringStyle, false)
return err
}

View File

@ -37,7 +37,8 @@
"home": "{{.global.home}}",
"remote": "{{.global.remote}}",
"branch": "{{.global.branch}}",
"style": "{{.global.style}}"
"style": "{{.global.style}}",
"test": "Generate test files"
},
"new": {
"short": "Fast create api service",