vscode-go icon indicating copy to clipboard operation
vscode-go copied to clipboard

tools: migrate test generation features to `gopls`

Open emil14 opened this issue 4 years ago • 12 comments

Is your feature request related to a problem? Please describe. I'm always frustrated when I click on "Generate test for function" and see "No tests were generated for ...".

Describe the solution you'd like I would like to see a reason of why there was no tests were generated. For example - "test for ... already exist".

Additional context It would be perfect to see a location of generated tests if there is already existing one. Or after successful generation.

emil14 avatar Jun 28 '21 10:06 emil14

@stamblerre I would like to work on this if no one's working.

n1lesh avatar Jul 13 '21 20:07 n1lesh

Sounds good! /cc @hyangah to confirm that this would be a good thing to work on

stamblerre avatar Jul 21 '21 17:07 stamblerre

Please correct me if I'm wrong, to generate tests vscode-go calls gotests lib internally as a child process.

However, gotests doesn't return any error if test(s) don't get generated other than "No tests were generated for...". The generateTest method (from gotests) ignores the func if test is already generated for it, and returns no error.

Probably modifying gotests to return an error for existing tests would be a better idea?

n1lesh avatar Jul 21 '21 18:07 n1lesh

@n1lesh: Thanks for digging into the underlying cause! An alternative option may actually be to move the test generation logic into gopls itself, so that we can adjust the behavior more easily. Would you be willing to work on a gopls change that adds a command to generate tests instead? I know that's a bigger ask than this fix, but I'd be happy to offer pointers if you're interested.

stamblerre avatar Jul 23 '21 17:07 stamblerre

@stamblerre I can work on the gopls change.

n1lesh avatar Jul 23 '21 18:07 n1lesh

The Gopls will handle the add unit tests request following steps below:

  • Gopls respond to code action request and add three new buttons including add unit test for selected functions, functions in this file and functions in this package. The implementation of those three buttons are another LSP rpc call add_unit_test to gopls.
image
  • The add unit test for functions button will only show up when the select range overlap with a top level (file level not inline) function/method declaration.
image
  • When user click on the button and request for unit test generation, the add_unit_test executeCommand rpc will be invoked and gopls will return WorkspaceEdit as response.

  • As documented, WorkspaceEdit allows us to return TextDocumentEdit | CreateFile. Gopls will return CreateFile only if the test file does not exist already and will follow the right sequence (create file, then edit the text).

NOTE:

  • The screenshot is only for demonstration purposes (is my local experiment).
  • The test file name will follow the convention FILE.go -> FILE_test.go.
  • The test name will follow the same convention as gotest. See method TestName defined in gotest repo for detail implementation.
  • For un-exported function, the test function name is Test_function(t *testing.T).
  • For exported function, the test function name is TestFunction(t *testing.T)
  • For un-exported struct's method. Test_foo_function(t *testing.T) and Test_foo_Function(t *testing.T)
  • For exported struct's method. TestFoo_function(t *testing.T) and TestFoo_Function(t *testing.T)
  • The receiver's type (pointer or struct) does not make any difference to the test function name.

@hyangah @adonovan @findleyr

h9jiang avatar Oct 07 '24 20:10 h9jiang

Cool! A few notes:

Gopls respond to code action request and add three new buttons including add unit test

Let's just say "Add unit test for X". No need to prefix the code action with "Source:", since it is in the "Source Action" section.

When user click on the button and request for unit test generation, the add_unit_test grpc will be invoked and gopls will return WorkspaceEdit as response.

s/grpc/rpc. Also, the command does not return a workspace edit. Rather it calls the workspace/applyEdit server->client rpc.

Test function naming LGTM.

It may be simpler to start with just "Add unit test for function".

findleyr avatar Oct 07 '24 21:10 findleyr

Excited to use this! A couple of suggestions:

  • The wording should be "Add" (since there may already be one), should avoid "unit", and should name the symbol. No "Source:" prefix is needed. So: Add test for function F or Add test for method T.M.
  • It should only pop up if the symbol is exported. We shouldn't encourage writing tests of internals.

adonovan avatar Oct 07 '24 21:10 adonovan

Add a source code action for single function. if the selected range is enclosed by a function decl or a method decl, we will provide this source code action to Add test for FUNCTION_NAME

Right now, the result of this action is returning a fake error.

image image

h9jiang avatar Oct 18 '24 15:10 h9jiang

Cool! Should probably be "Add a test for Bar", but we can discuss that on the CL. This will be great.

findleyr avatar Oct 18 '24 18:10 findleyr

Change https://go.dev/cl/621056 mentions this issue: gopls/internal/golang: add source code action for add test

gopherbot avatar Oct 18 '24 18:10 gopherbot

Change https://go.dev/cl/621096 mentions this issue: gopls/internal/golang: create test file if not exist

gopherbot avatar Oct 18 '24 23:10 gopherbot

Next CL: Following the existing implementation from gotest. When adding for a method, first declare a struct have all the fields as the receiver type.

For embedding type, we will choose the type name as fields name. E.g. Field Node for type ast.Node since struct bar does not provide a name for field which have type ast.Node.

func Test_foo_String(t *testing.T) {
	type fields struct {                  // <----- fields declaration.                
		Node       ast.Node 
		barinside  barinside
		inside     int
		insidecopy int
	}              
        ...
...
type bar struct {
	ast.Node
	barinside
	inside     int
	insidecopy int
}

type foo bar

func (*foo) String() string {return ""}

The end goal of the test looks below. (This is generate by the gotest)

func Test_foo_String(t *testing.T) {
	type fields struct {                          
		Node       ast.Node
		barinside  barinside
		inside     int
		insidecopy int
	}
	tests := []struct {
		name   string
		fields fields
		want   string
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			f := &foo{
				Node:       tt.fields.Node,
				barinside:  tt.fields.barinside,
				inside:     tt.fields.inside,
				insidecopy: tt.fields.insidecopy,
			}
			if got := f.String(); got != tt.want {
				t.Errorf("foo.String() = %v, want %v", got, tt.want)
			}
		})
	}
}

h9jiang avatar Oct 21 '24 17:10 h9jiang

Change https://go.dev/cl/621675 mentions this issue: gopls/internal/golang: add type decl including receiver's type fields

gopherbot avatar Oct 21 '24 18:10 gopherbot

To update this issue with some decisions:

  1. If the FILE_test.go does not exist, we will generate FILE_test.go with package PACKAGE_test. This follows the best practices where we want to test only the public APIs.
  2. If the FILE_test.go does exist, we will generate test based on the specified package. If the package name is PACKAGE, we will allow generating un-exported functions or methods. If the package name is PACKAGE_test, we will generate only exported functions or methods. If the package name is not PACKAGE or PACKAGE_test, we will not generate any test for any functions/methods.
  3. When adding test for a method, we will try to figure out the constructor instead of composite literals. For T's method, we will try to find any function that match one of the following signature (only allow if the constructor returns at most two values)
func NewT(....) T // returns T
func NewT(....) (T, any) // returns T, any 
func NewT(....) *T // returns *T
func NewT(....) (*T, any) // returns *T, any
  1. If there are multiple return values from the constructor, we can try our best to handle if the second return value is error t, err := NewT(...) otherwise, drop using t, _ := NewT(...)
  2. When adding test for PACKAGE, we will allow constructor newT.
  3. After getting the return value from the functions/methods, we will leave the comparison to the user. The comparison is entirely determined by the context, we can never find an ideal comparison function between reflect.DeepEqual, ==, cmp.Diff. We will leave a comment and ask the user to write the comparison function. (except for the error return type, will explain in the next point)
      // TODO: update the condition below to compare with test.want.
      if true {
        t.Errorf("Foo(%d) = %s, want %s", test.in, got, test.want)
      }
  1. If the function/method returns an error, there are some special handling. The test struct will have a field wantErr bool and correspondingly, a comparison block will be created to make sure the wantErr behavior matches with the returned error.
  got, got1, gotErr := Foo()
  if (err != nil) != test.wantErr {
    t.Errorf("Foo() got err %v, want error: %t", gotErr, test.wantErr)
  }

@hyangah @findleyr @adonovan

h9jiang avatar Oct 22 '24 19:10 h9jiang

All good, though in item 7 I suggest splitting the two cases as it leads to better error messages. (In Google readability reviews people sometimes say "but it's longer", but this is generated code so that's excuse won't fly. ;-)

  got, err := Foo(args)
  if err != nil {
        if !test.wantErr {
            t.Errorf("Foo(%v) failed: %v", args, err)
        }
        return
  }
  if test.wantErr {
      t.Fatalf("Foo(%v) succeeded unexpectedly", args)
  }
  ... compare got and test.want ...

adonovan avatar Oct 22 '24 19:10 adonovan

Change https://go.dev/cl/621057 mentions this issue: gopls/internal/golang: generate test for function

gopherbot avatar Oct 24 '24 18:10 gopherbot

Change https://go.dev/cl/622320 mentions this issue: gopls/internal/golang: create imports map honor x_test.go over x.go

gopherbot avatar Oct 25 '24 19:10 gopherbot

Change https://go.dev/cl/623997 mentions this issue: gopls/internal/golang: add special handling for last return error

gopherbot avatar Nov 01 '24 18:11 gopherbot

Change https://go.dev/cl/621816 mentions this issue: gopls/internal/golang: run testcases as subtests

gopherbot avatar Nov 04 '24 18:11 gopherbot

Change https://go.dev/cl/620696 mentions this issue: gopls/internal/golang: support generating test for method w/o constructor

gopherbot avatar Nov 05 '24 19:11 gopherbot

Change https://go.dev/cl/620697 mentions this issue: gopls/internal/golang: add missing imports in foo_test.go

gopherbot avatar Nov 07 '24 21:11 gopherbot

Change https://go.dev/cl/626537 mentions this issue: gopls/internal/golang, go/ssa: remove unnamed input parameter

gopherbot avatar Nov 11 '24 23:11 gopherbot

Change https://go.dev/cl/627355 mentions this issue: gopls/internal/golang: special handling for input context.Context

gopherbot avatar Nov 12 '24 22:11 gopherbot

Change https://go.dev/cl/629756 mentions this issue: gopls/internal/golang: preserve copyright and build constraint

gopherbot avatar Nov 19 '24 23:11 gopherbot

Change https://go.dev/cl/630119 mentions this issue: gopls/doc/features: add feature documentation for source.addTest

gopherbot avatar Nov 20 '24 20:11 gopherbot

Change https://go.dev/cl/629978 mentions this issue: gopls/internal/golang: improve test package name selection for new file

gopherbot avatar Nov 21 '24 19:11 gopherbot

Change https://go.dev/cl/632857 mentions this issue: gopls/internal/golang: Disable test generation for generic functions

gopherbot avatar Dec 02 '24 17:12 gopherbot

Change https://go.dev/cl/633703 mentions this issue: gopls/internal/golang: Disable test generation for generic functions

gopherbot avatar Dec 04 '24 19:12 gopherbot

Change https://go.dev/cl/692055 mentions this issue: gopls/internal/golang: show document after test generation

gopherbot avatar Jul 31 '25 16:07 gopherbot