Skip to content

Commit 51e54e8

Browse files
h9jianggopherbot
authored andcommitted
gopls/doc/features: enable and document source.addTest code action
- Add feature documentation and demo screenshot. - Remove the internal feature gates guarding add test feature. - Replace spaces with tabs in test function body template. - Run format for generated test function body. For golang/vscode-go#1594 Change-Id: Id2106654c24da61ad6aa31723908e4931aa54430 Reviewed-on: https://go-review.googlesource.com/c/tools/+/630119 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Hongxiang Jiang <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 458067f commit 51e54e8

File tree

11 files changed

+1032
-990
lines changed

11 files changed

+1032
-990
lines changed
304 KB
Loading

gopls/doc/features/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ when making significant changes to existing features or when adding new ones.
5050
- [Extract](transformation.md#refactor.extract): extract selection to a new file/function/variable
5151
- [Inline](transformation.md#refactor.inline.call): inline a call to a function or method
5252
- [Miscellaneous rewrites](transformation.md#refactor.rewrite): various Go-specific refactorings
53+
- [Add test for func](transformation.md#source.addTest): create a test for the selected function
5354
- [Web-based queries](web.md): commands that open a browser page
5455
- [Package documentation](web.md#doc): browse documentation for current Go package
5556
- [Free symbols](web.md#freesymbols): show symbols used by a selected block of code

gopls/doc/features/transformation.md

+40
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Gopls supports the following code actions:
6767
- [`source.doc`](web.md#doc)
6868
- [`source.freesymbols`](web.md#freesymbols)
6969
- `source.test` (undocumented) <!-- TODO: fix that -->
70+
- [`source.addTest`](#source.addTest)
7071
- [`gopls.doc.features`](README.md), which opens gopls' index of features in a browser
7172
- [`refactor.extract.function`](#extract)
7273
- [`refactor.extract.method`](#extract)
@@ -210,6 +211,45 @@ Client support:
210211
```
211212
- **CLI**: `gopls fix -a file.go:#offset source.organizeImports`
212213

214+
<a name='source.addTest'></a>
215+
## `source.addTest`: Add test for function or method
216+
217+
If the selected chunk of code is part of a function or method declaration F,
218+
gopls will offer the "Add test for F" code action, which adds a new test for the
219+
selected function in the corresponding `_test.go` file. The generated test takes
220+
into account its signature, including input parameters and results.
221+
222+
**Test file**: if the `_test.go` file does not exist, gopls creates it, based on
223+
the name of the current file (`a.go` -> `a_test.go`), copying any copyright and
224+
build constraint comments from the original file.
225+
226+
**Test package**: for new files that test code in package `p`, the test file
227+
uses `p_test` package name whenever possible, to encourage testing only exported
228+
functions. (If the test file already exists, the new test is added to that file.)
229+
230+
**Parameters**: each of the function's non-blank parameters becomes an item in
231+
the struct used for the table-driven test. (For each blank `_` parameter, the
232+
value has no effect, so the test provides a zero-valued argument.)
233+
234+
**Contexts**: If the first parameter is `context.Context`, the test passes
235+
`context.Background()`.
236+
237+
**Results**: the function's results are assigned to variables (`got`, `got2`,
238+
and so on) and compared with expected values (`want`, `want2`, etc.`) defined in
239+
the test case struct. The user should edit the logic to perform the appropriate
240+
comparison. If the final result is an `error`, the test case defines a `wantErr`
241+
boolean.
242+
243+
**Method receivers**: When testing a method `T.F` or `(*T).F`, the test must
244+
construct an instance of T to pass as the receiver. Gopls searches the package
245+
for a suitable function that constructs a value of type T or *T, optionally with
246+
an error, preferring a function named `NewT`.
247+
248+
**Imports**: Gopls adds missing imports to the test file, using the last
249+
corresponding import specifier from the original file. It avoids duplicate
250+
imports, preserving any existing imports in the test file.
251+
252+
<img title="Add test for func" src="../assets/add-test-for-func.png" width='80%'>
213253

214254
<a name='rename'></a>
215255
## Rename

gopls/doc/release/v0.17.0.md

+12
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,15 @@ from the context of the call.
8484
The new `yield` analyzer detects mistakes using the `yield` function
8585
in a Go 1.23 iterator, such as failure to check its boolean result and
8686
break out of a loop.
87+
88+
## Add test for function or method
89+
90+
If the selected chunk of code is part of a function or method declaration F,
91+
gopls will offer the "Add test for F" code action, which adds a new test for the
92+
selected function in the corresponding `_test.go` file. The generated test takes
93+
into account its signature, including input parameters and results.
94+
95+
Since this feature is implemented by the server (gopls), it is compatible with
96+
all LSP-compliant editors. VS Code users may continue to use the client-side
97+
`Go: Generate Unit Tests For file/function/package` command which utilizes the
98+
[gotests](https://github.com/cweill/gotests) tool.

gopls/internal/golang/addtest.go

+126-120
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"errors"
1313
"fmt"
1414
"go/ast"
15+
"go/format"
1516
"go/token"
1617
"go/types"
1718
"os"
@@ -34,124 +35,124 @@ import (
3435

3536
const testTmplString = `
3637
func {{.TestFuncName}}(t *{{.TestingPackageName}}.T) {
37-
{{- /* Test cases struct declaration and empty initialization. */}}
38-
tests := []struct {
39-
name string // description of this test case
40-
41-
{{- $commentPrinted := false }}
42-
{{- if and .Receiver .Receiver.Constructor}}
43-
{{- range .Receiver.Constructor.Args}}
44-
{{- if .Name}}
45-
{{- if not $commentPrinted}}
46-
// Named input parameters for receiver constructor.
47-
{{- $commentPrinted = true }}
48-
{{- end}}
49-
{{.Name}} {{.Type}}
50-
{{- end}}
51-
{{- end}}
52-
{{- end}}
53-
54-
{{- $commentPrinted := false }}
55-
{{- range .Func.Args}}
56-
{{- if .Name}}
57-
{{- if not $commentPrinted}}
58-
// Named input parameters for target function.
59-
{{- $commentPrinted = true }}
60-
{{- end}}
61-
{{.Name}} {{.Type}}
62-
{{- end}}
63-
{{- end}}
64-
65-
{{- range $index, $res := .Func.Results}}
66-
{{- if eq $res.Name "gotErr"}}
67-
wantErr bool
68-
{{- else if eq $index 0}}
69-
want {{$res.Type}}
70-
{{- else}}
71-
want{{add $index 1}} {{$res.Type}}
72-
{{- end}}
73-
{{- end}}
74-
}{
75-
// TODO: Add test cases.
76-
}
77-
78-
{{- /* Loop over all the test cases. */}}
79-
for _, tt := range tests {
80-
t.Run(tt.name, func(t *{{.TestingPackageName}}.T) {
81-
{{- /* Constructor or empty initialization. */}}
82-
{{- if .Receiver}}
83-
{{- if .Receiver.Constructor}}
84-
{{- /* Receiver variable by calling constructor. */}}
85-
{{fieldNames .Receiver.Constructor.Results ""}} := {{if .PackageName}}{{.PackageName}}.{{end}}
86-
{{- .Receiver.Constructor.Name}}
87-
88-
{{- /* Constructor input parameters. */ -}}
89-
(
90-
{{- range $index, $arg := .Receiver.Constructor.Args}}
91-
{{- if ne $index 0}}, {{end}}
92-
{{- if .Name}}tt.{{.Name}}{{else}}{{.Value}}{{end}}
93-
{{- end -}}
94-
)
95-
96-
{{- /* Handles the error return from constructor. */}}
97-
{{- $last := last .Receiver.Constructor.Results}}
98-
{{- if eq $last.Type "error"}}
99-
if err != nil {
100-
t.Fatalf("could not construct receiver type: %v", err)
101-
}
102-
{{- end}}
103-
{{- else}}
104-
{{- /* Receiver variable declaration. */}}
105-
// TODO: construct the receiver type.
106-
var {{.Receiver.Var.Name}} {{.Receiver.Var.Type}}
107-
{{- end}}
108-
{{- end}}
109-
110-
{{- /* Got variables. */}}
111-
{{if .Func.Results}}{{fieldNames .Func.Results ""}} := {{end}}
112-
113-
{{- /* Call expression. */}}
114-
{{- if .Receiver}}{{/* Call method by VAR.METHOD. */}}
115-
{{- .Receiver.Var.Name}}.
116-
{{- else if .PackageName}}{{/* Call function by PACKAGE.FUNC. */}}
117-
{{- .PackageName}}.
118-
{{- end}}{{.Func.Name}}
119-
120-
{{- /* Input parameters. */ -}}
121-
(
122-
{{- range $index, $arg := .Func.Args}}
123-
{{- if ne $index 0}}, {{end}}
124-
{{- if .Name}}tt.{{.Name}}{{else}}{{.Value}}{{end}}
125-
{{- end -}}
126-
)
127-
128-
{{- /* Handles the returned error before the rest of return value. */}}
129-
{{- $last := last .Func.Results}}
130-
{{- if eq $last.Type "error"}}
131-
if gotErr != nil {
132-
if !tt.wantErr {
133-
t.Errorf("{{$.Func.Name}}() failed: %v", gotErr)
134-
}
135-
return
136-
}
137-
if tt.wantErr {
138-
t.Fatal("{{$.Func.Name}}() succeeded unexpectedly")
139-
}
140-
{{- end}}
141-
142-
{{- /* Compare the returned values except for the last returned error. */}}
143-
{{- if or (and .Func.Results (ne $last.Type "error")) (and (gt (len .Func.Results) 1) (eq $last.Type "error"))}}
144-
// TODO: update the condition below to compare got with tt.want.
145-
{{- range $index, $res := .Func.Results}}
146-
{{- if ne $res.Name "gotErr"}}
147-
if true {
148-
t.Errorf("{{$.Func.Name}}() = %v, want %v", {{.Name}}, tt.{{if eq $index 0}}want{{else}}want{{add $index 1}}{{end}})
149-
}
150-
{{- end}}
151-
{{- end}}
152-
{{- end}}
153-
})
154-
}
38+
{{- /* Test cases struct declaration and empty initialization. */}}
39+
tests := []struct {
40+
name string // description of this test case
41+
42+
{{- $commentPrinted := false }}
43+
{{- if and .Receiver .Receiver.Constructor}}
44+
{{- range .Receiver.Constructor.Args}}
45+
{{- if .Name}}
46+
{{- if not $commentPrinted}}
47+
// Named input parameters for receiver constructor.
48+
{{- $commentPrinted = true }}
49+
{{- end}}
50+
{{.Name}} {{.Type}}
51+
{{- end}}
52+
{{- end}}
53+
{{- end}}
54+
55+
{{- $commentPrinted := false }}
56+
{{- range .Func.Args}}
57+
{{- if .Name}}
58+
{{- if not $commentPrinted}}
59+
// Named input parameters for target function.
60+
{{- $commentPrinted = true }}
61+
{{- end}}
62+
{{.Name}} {{.Type}}
63+
{{- end}}
64+
{{- end}}
65+
66+
{{- range $index, $res := .Func.Results}}
67+
{{- if eq $res.Name "gotErr"}}
68+
wantErr bool
69+
{{- else if eq $index 0}}
70+
want {{$res.Type}}
71+
{{- else}}
72+
want{{add $index 1}} {{$res.Type}}
73+
{{- end}}
74+
{{- end}}
75+
}{
76+
// TODO: Add test cases.
77+
}
78+
79+
{{- /* Loop over all the test cases. */}}
80+
for _, tt := range tests {
81+
t.Run(tt.name, func(t *{{.TestingPackageName}}.T) {
82+
{{- /* Constructor or empty initialization. */}}
83+
{{- if .Receiver}}
84+
{{- if .Receiver.Constructor}}
85+
{{- /* Receiver variable by calling constructor. */}}
86+
{{fieldNames .Receiver.Constructor.Results ""}} := {{if .PackageName}}{{.PackageName}}.{{end}}
87+
{{- .Receiver.Constructor.Name}}
88+
89+
{{- /* Constructor input parameters. */ -}}
90+
(
91+
{{- range $index, $arg := .Receiver.Constructor.Args}}
92+
{{- if ne $index 0}}, {{end}}
93+
{{- if .Name}}tt.{{.Name}}{{else}}{{.Value}}{{end}}
94+
{{- end -}}
95+
)
96+
97+
{{- /* Handles the error return from constructor. */}}
98+
{{- $last := last .Receiver.Constructor.Results}}
99+
{{- if eq $last.Type "error"}}
100+
if err != nil {
101+
t.Fatalf("could not construct receiver type: %v", err)
102+
}
103+
{{- end}}
104+
{{- else}}
105+
{{- /* Receiver variable declaration. */}}
106+
// TODO: construct the receiver type.
107+
var {{.Receiver.Var.Name}} {{.Receiver.Var.Type}}
108+
{{- end}}
109+
{{- end}}
110+
111+
{{- /* Got variables. */}}
112+
{{if .Func.Results}}{{fieldNames .Func.Results ""}} := {{end}}
113+
114+
{{- /* Call expression. */}}
115+
{{- if .Receiver}}{{/* Call method by VAR.METHOD. */}}
116+
{{- .Receiver.Var.Name}}.
117+
{{- else if .PackageName}}{{/* Call function by PACKAGE.FUNC. */}}
118+
{{- .PackageName}}.
119+
{{- end}}{{.Func.Name}}
120+
121+
{{- /* Input parameters. */ -}}
122+
(
123+
{{- range $index, $arg := .Func.Args}}
124+
{{- if ne $index 0}}, {{end}}
125+
{{- if .Name}}tt.{{.Name}}{{else}}{{.Value}}{{end}}
126+
{{- end -}}
127+
)
128+
129+
{{- /* Handles the returned error before the rest of return value. */}}
130+
{{- $last := last .Func.Results}}
131+
{{- if eq $last.Type "error"}}
132+
if gotErr != nil {
133+
if !tt.wantErr {
134+
t.Errorf("{{$.Func.Name}}() failed: %v", gotErr)
135+
}
136+
return
137+
}
138+
if tt.wantErr {
139+
t.Fatal("{{$.Func.Name}}() succeeded unexpectedly")
140+
}
141+
{{- end}}
142+
143+
{{- /* Compare the returned values except for the last returned error. */}}
144+
{{- if or (and .Func.Results (ne $last.Type "error")) (and (gt (len .Func.Results) 1) (eq $last.Type "error"))}}
145+
// TODO: update the condition below to compare got with tt.want.
146+
{{- range $index, $res := .Func.Results}}
147+
{{- if ne $res.Name "gotErr"}}
148+
if true {
149+
t.Errorf("{{$.Func.Name}}() = %v, want %v", {{.Name}}, tt.{{if eq $index 0}}want{{else}}want{{add $index 1}}{{end}})
150+
}
151+
{{- end}}
152+
{{- end}}
153+
{{- end}}
154+
})
155+
}
155156
}
156157
`
157158

@@ -250,7 +251,7 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol.
250251
for _, spec := range file.Imports {
251252
// TODO(hxjiang): support dot imports.
252253
if spec.Name != nil && spec.Name.Name == "." {
253-
return nil, fmt.Errorf("\"add a test for func\" does not support files containing dot imports")
254+
return nil, fmt.Errorf("\"add test for func\" does not support files containing dot imports")
254255
}
255256
path, err := strconv.Unquote(spec.Path.Value)
256257
if err != nil {
@@ -783,10 +784,15 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol.
783784
return nil, err
784785
}
785786

787+
formatted, err := format.Source(test.Bytes())
788+
if err != nil {
789+
return nil, err
790+
}
791+
786792
edits = append(edits,
787793
protocol.TextEdit{
788794
Range: eofRange,
789-
NewText: test.String(),
795+
NewText: string(formatted),
790796
})
791797

792798
return append(changes, protocol.DocumentChangeEdit(testFH, edits)), nil

gopls/internal/golang/codeaction.go

+2-7
Original file line numberDiff line numberDiff line change
@@ -479,14 +479,9 @@ func refactorExtractToNewFile(ctx context.Context, req *codeActionsRequest) erro
479479
return nil
480480
}
481481

482-
// addTest produces "Add a test for FUNC" code actions.
482+
// addTest produces "Add test for FUNC" code actions.
483483
// See [server.commandHandler.AddTest] for command implementation.
484484
func addTest(ctx context.Context, req *codeActionsRequest) error {
485-
// Reject if the feature is turned off.
486-
if !req.snapshot.Options().AddTestSourceCodeAction {
487-
return nil
488-
}
489-
490485
// Reject test package.
491486
if req.pkg.Metadata().ForTest != "" {
492487
return nil
@@ -507,7 +502,7 @@ func addTest(ctx context.Context, req *codeActionsRequest) error {
507502
return nil
508503
}
509504

510-
cmd := command.NewAddTestCommand("Add a test for "+decl.Name.String(), req.loc)
505+
cmd := command.NewAddTestCommand("Add test for "+decl.Name.String(), req.loc)
511506
req.addCommandAction(cmd, true)
512507

513508
// TODO(hxjiang): add code action for generate test for package/file.

gopls/internal/protocol/command/interface.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ type Interface interface {
238238
// to avoid conflicts with other counters gopls collects.
239239
AddTelemetryCounters(context.Context, AddTelemetryCountersArgs) error
240240

241-
// AddTest: add a test for the selected function
241+
// AddTest: add test for the selected function
242242
AddTest(context.Context, protocol.Location) (*protocol.WorkspaceEdit, error)
243243

244244
// MaybePromptForTelemetry: Prompt user to enable telemetry

gopls/internal/settings/default.go

-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ func DefaultOptions(overrides ...func(*Options)) *Options {
136136
LinkifyShowMessage: false,
137137
IncludeReplaceInWorkspace: false,
138138
ZeroConfig: true,
139-
AddTestSourceCodeAction: false,
140139
},
141140
}
142141
})

0 commit comments

Comments
 (0)