ChatGPT generates unit test practice (Golang)

foreword

At present, gpt is essentially a continuation, so when the function to be tested is clearly defined, unit tests can be generated appropriately depending on it.

What is the benefit:

  1. Assist in the generation of test cases & test codes to reduce the mental cost of unit test writing
  2. Auxiliary code review to help discover code explicit/potential problems

The test environment for this article:

  • gpt: gpt-3.5-turbo
  • go:go 1.17

Practice scenario of this article: Enterprise WeChat Meitu Appreciation Robot


The workflow for generating unit tests is as follows:

  1. Select your function to be tested
  2. Analyze function dependencies: structure definition, repo dependencies, repo interface abstraction (it will be much more convenient to use dependency injection frameworks such as fx or wire)
  3. Organization prompt, ready to cast spells
  4. Sing the magic, get the output unit test code
  5. Copy to the ide for sewing and repairing. If there are too many discrepancies with expectations, adjust the prompt and restart/require gpt to rewrite according to a certain standard (for example: please rewrite the organization test data according to the testCases method)
  6. run test case

Not much to say, let's start with the basic prompt:

写下列写下列代码中[待测函数名]函数的单测,要求覆盖全部逻辑。 
断言使用require包。 
测试用例以testCases的形式输出。

In the following part, I simply distinguish the easy/normal/hard situations according to the complexity of the workflow.


Part1 easy: single function, no complicated dependencies

A single function, and the external dependencies are all open source libraries, so just paste the code directly.

Here we use the independent function of image compression as an example:
(prompt front shake mantra + code snippet)

写下列代码中CompressPicture函数的单测,要求覆盖全部逻辑。
断言使用require包。
测试用例以testCases的形式输出。

// CompressPicture the picture by resizing it to the specified width.
func CompressPicture(picData []byte, width uint) ([]byte, error) {
   // Decode the input image.
   oldBuffer := bytes.NewBuffer(picData)
   inputImg, _, err := image.Decode(oldBuffer)
   if err != nil {
      return nil, err
   }
   // Resize the image to the specified width.
   outputImg := resize.Resize(width, 0, inputImg, resize.Lanczos3)
   // Encode the output image as PNG and write it to a buffer.
   newBuffer := new(bytes.Buffer)
   err = png.Encode(newBuffer, outputImg)
   if err != nil {
      return nil, err
   }
   // Return the compressed image as a byte slice.
   return newBuffer.Bytes(), nil
}

The code block output by ChatGPT is as follows:

func TestCompressPicture(t *testing.T) {
   // Setup test data
   imgData, err := os.ReadFile("test-image.png")
   if err != nil {
      t.Fatalf("failed to read test image file: %v", err)
   }
   testCases := []struct {
      name  string
      width uint
   }{
      {"compress with a valid width", 500},
      {"compress with an invalid width", 0},
   }
   for _, tc := range testCases {
      t.Run(tc.name, func(t *testing.T) {
         // Invoke the function being tested.
         compressedData, err := CompressPicture(imgData, tc.width)

         // Check the result.
         if err != nil {
            t.Fatalf("unexpected error: %v", err)
         }
         if len(compressedData) == 0 {
            t.Fatalf("compressed data is empty")
         }
         create, err := os.Create("test-image-compressed.png")
         if err != nil {
            return
         }
         defer create.Close()
         _, err = create.Write(compressedData)
      })

   }
}

Putting it in the ide, it runs perfectly, which shows that the unit test generation of simple functions is still very strong~

pass


Part2 normal: There are some external imports inside

When an external definition is introduced, it is best to paste the external definition directly (no mock is involved).

The function exemplified here has external dependencies such as entity.ArchiveWithData, so it is best to add external definitions to the input:

写下列代码中ConvertArchiveWithDataToBotTextMsg函数的单测,要求覆盖全部逻辑。
断言使用require包。
测试用例以testCases的形式输出。

// ConvertArchiveWithDataToBotTextMsg 将画作稿件信息转换为企微机器人文本消息
func ConvertArchiveWithDataToBotTextMsg(data *entity.ArchiveWithData) *entity.BotMsgReq {
   var MentionedList []string
   proxyUrl := data.Info.Urls.Original
   rawPixivUrl := fmt.Sprintf("https://www.pixiv.net/artworks/%d", data.Info.Pid)
   txt := &entity.BotText{
      Content:       fmt.Sprintf("proxy图源:%s\npixiv图源:%s", proxyUrl, rawPixivUrl),
      MentionedList: MentionedList,
   }
   postText := &entity.BotMsgReq{
      MsgType: entity.BotMsgText,
      Text:    txt,
   }
   return postText
}

一些导入的定义如下:
type BotText struct {
   Content       string   `json:"content"`
   MentionedList []string `json:"mentioned_list"`
}
type BotMsgReq struct {
   MsgType BotMsgType `json:"msgtype"`
   Text    *BotText   `json:"text,omitempty"`
}
const (
   BotMsgText  BotMsgType = "text"
)

type ArchiveWithData struct {
   Info Archive
   Data []byte
}

The following is the generated code, which can be run directly after a small modification:
(Because the definition of a structure is missing, it guesses some fields)

func Test_ConvertArchiveWithDataToBotTextMsg(t *testing.T) {
   testCases := []struct {
      name     string
      data     *entity.ArchiveWithData
      expected *entity.BotMsgReq
   }{
      {
         name: "regular data",
         data: &entity.ArchiveWithData{
            Info: entity.Archive{
               Pid: 123456,
               Urls: entity.PicUrl{
                  Original: "https://example.com/image.jpg",
               },
            },
            Data: []byte{},
         },
         expected: &entity.BotMsgReq{
            MsgType: entity.BotMsgText,
            Text: &entity.BotText{
               Content: "proxy图源:https://example.com/image.jpg\npixiv图源:https://www.pixiv.net/artworks/123456",
            },
         },
      },
      {
         name: "data with special characters",
         data: &entity.ArchiveWithData{
            Info: entity.Archive{
               Pid: 789012,
               Urls: entity.PicUrl{
                  Original: "https://example.com/image(with)special&characters.jpg",
               },
            },
            Data: []byte{},
         },
         expected: &entity.BotMsgReq{
            MsgType: entity.BotMsgText,
            Text: &entity.BotText{
               Content: "proxy图源:https://example.com/image(with)special&characters.jpg\npixiv图源:https://www.pixiv.net/artworks/789012",
            },
         },
      },
   }

   for _, tc := range testCases {
      t.Run(tc.name, func(t *testing.T) {
         result := ConvertArchiveWithDataToBotTextMsg(tc.data)
         require.Equal(t, tc.expected, result)
      })
   }
}

Running smoothly after minor fixes


Part3 hard: Mock external repo (gomock example)

The more external dependencies there are, the more complicated the construction of the prompt template may be.
ps. In fact, gomonkey can also write
the part of the external dependency repo, requiring gpt to use gomock to mock the corresponding repo.
Outer definitions are best pruned.

enter:

写下列代码中getSeTuDescSlice函数的单测,要求覆盖全部逻辑。
断言使用require包。
测试用例以testCases的形式输出。
对于m.repoSeTu.GetArchiveInfoSlice的调用,使用gomock进行mock处理,假设已经有mock文件。

func (m *CronTab) getSeTuDescSlice(ctx context.Context, seTuConfig config.SeTuConfig) (entity.QueryResult, error) {
    
    
   queryResSlice, err := m.repoSeTu.GetArchiveInfoSlice(ctx, &entity.Query{
    
    
      R18:   seTuConfig.R18,
      Num:   1,
      Tag:   seTuConfig.Tags,
      Size:  seTuConfig.PicSize,
      Proxy: seTuConfig.Proxy,
   })
   if err != nil {
    
    
      return entity.QueryResult{
    
    }, err
   }
   return queryResSlice, nil
}

下面是外部依赖的定义:
type SeTuConfig struct {
    
    
   SetuApiUrl string   `yaml:"setu_api_url"`
   QWAuth     []string `yaml:"qw_auth"`
   CronStr    string   `yaml:"cron_str"`
   R18        int64    `yaml:"r_18"`
   Tags       []string `yaml:"tags"`
   PicSize    []string `yaml:"pic_size"`
   Proxy      string   `yaml:"proxy"`
}

// QueryResult 请求结果
type QueryResult struct {
    
    
   Error        string    `json:"error"`
   ArchiveSlice []Archive `json:"data"`
   //picPaths     []string
}

output:

假设已经有mock文件:

// mock_repoSetu 是通过mockgen自动生成的,用于模拟repoSetu接口的mock文件

func TestCronTab_getSeTuDescSlice(t *testing.T) {
    
    
    testCases := []struct{
    
    
        name string
        seTuConfig config.SeTuConfig
        getArchiveInfoSliceRes entity.QueryResult
        getArchiveInfoSliceErr error
        wantErr bool
    }{
    
    
        {
    
    
            name: "success",
            seTuConfig: config.SeTuConfig{
    
    
                R18: 0,
                Tags: []string{
    
    "tag1", "tag2"},
                PicSize: []string{
    
    "large"},
                Proxy: "",
            },
            getArchiveInfoSliceRes: entity.QueryResult{
    
    
                ArchiveSlice: []entity.Archive{
    
    
                    {
    
    
                        Urls: []string{
    
    "pic1.url", "pic2.url"},
                        Title: "pic1",
                        Artists: []string{
    
    "artist1"},
                    },
                },
            },
            getArchiveInfoSliceErr: nil,
            wantErr: false,
        },
        {
    
    
            name: "error",
            seTuConfig: config.SeTuConfig{
    
    
                R18: 1,
                Tags: []string{
    
    "tag3"},
                PicSize: []string{
    
    "small", "medium"},
                Proxy: "",
            },
            getArchiveInfoSliceRes: entity.QueryResult{
    
    },
            getArchiveInfoSliceErr: errors.New("get archive info slice error"),
            wantErr: true,
        },
    }

    for _, tc := range testCases {
    
    
        t.Run(tc.name, func(t *testing.T) {
    
    
            ctx := context.Background()

            // 创建mock对象
            mockRepoSeTu := mock_repoSetu.NewMockRepoSeTu(ctrl)

            m := &CronTab{
    
    repoSeTu: mockRepoSeTu}

            // 设置mock对象返回值
            mockRepoSeTu.EXPECT().GetArchiveInfoSlice(ctx, &entity.Query{
    
    
                R18: tc.seTuConfig.R18,
                Num: 1,
                Tag: tc.seTuConfig.Tags,
                Size: tc.seTuConfig.PicSize,
                Proxy: tc.seTuConfig.Proxy,
            }).Return(tc.getArchiveInfoSliceRes, tc.getArchiveInfoSliceErr)

            res, err := m.getSeTuDescSlice(ctx, tc.seTuConfig)
            if tc.wantErr {
    
    
                require.Error(t, err)
            } else {
    
    
                require.NoError(t, err)
                require.Equal(t, tc.getArchiveInfoSliceRes, res)
            }
        })
    }
}

测试用例解释:
测试用例"success"模拟了getSeTuDescSlice调用成功的情况,
设置查询条件R18=0,Tags="tag1", "tag2",PicSize="large",期望返回多条图片信息;
测试用例"error"模拟了getSeTuDescSlice调用失败的情况,
设置查询条件R18=1,Tags="tag3",PicSize="small", "medium",期望返回错误信息。

The generated code is too long, a few summary:

  1. The gomock code can be generated normally, but the external repo definition should be clear and clear, you can directly paste the interface definition, and it will be recognized.
  2. Paste it into the ide, and found that the main problem is the import of the package name, which can be simply modified

The generated code has some errors:
some error

After modifying the imported type name:
Minor repairs

After modification, it runs successfully:
run!

If the amount of artificial modification after generation is required to be lower, more precise context information (package name, function definition, description, etc.) is required


some pain points

  1. ChatGPT has a length limit for a single conversation, so try to generate unit tests for only one function at a time.
  2. Functions under test that require external repo mocks require a lot of context, and the introduced irrelevant fields need to be pruned to avoid polluting the input
  3. Some complex private packages seem to be difficult to support (I tried to teach him in the prompt, but the effect is not good)
  4. For some complex logic and boundary conditions, AI may say something wrong (the written use case is inconsistent with the expression, such as the classic 20%*20%=400%, but he knows it is wrong)
  5. For projects whose layers are not very clear, it may be difficult to pile functions/repos. At this time, it is more difficult to describe clearly. gomonkey may be more suitable

other usage

  1. Write a good test method and let gpt help expand the use case set
  2. code review, the following code is the code suggestion given by gpt
帮我优化下面的代码

// picCompress Modify size to compress pictures.
func picCompress(picData []byte) (newPicData []byte, err error) {
    
    
   oldBuf := bytes.NewBuffer(picData)
   pic, _, err := image.Decode(oldBuf)
   if err != nil {
    
    
      return
   }
   newPic := resize.Resize(uint(pic.Bounds().Dx()/2), 0, pic, resize.Lanczos3)
   var newBuf bytes.Buffer
   err = png.Encode(&newBuf, newPic)
   if err != nil {
    
    
      return
   }
   newPicData, err = ioutil.ReadAll(&newBuf)
   if err != nil {
    
    
      return
   }
   return
}
您好!下面是一个优化版的代码,主要改进在于:
减少对象的创建,避免额外的内存分配和垃圾回收。
增加错误检查,避免可能的错误被忽略。
以下是优化版代码:
// picCompress Modify size to compress pictures.
func picCompress(picData []byte) ([]byte, error) {
    
    
    reader := bytes.NewReader(picData)
    pic, _, err := image.Decode(reader)
    if err != nil {
    
    
        return nil, err
    }
    newPic := resize.Resize(uint(pic.Bounds().Dx()/2), 0, pic, resize.Lanczos3)
    var writer bytes.Buffer
    err = png.Encode(&writer, newPic)
    if err != nil {
    
    
        return nil, err
    }
    return writer.Bytes(), nil
}

尝试减少了对象的创建,使用了 bytes.Reader 代替了 bytes.NewBuffer 和 ioutil.ReadAll。
在处理图片过程中,使用 bytes.Buffer 代替了 newBuf 这个额外的对象。
同时也将函数的返回值简化,只返回处理后的 []byte 和可能的错误,避免了空值的返回。最后,增加错误检查,保证代码健壮性。

Guess you like

Origin blog.csdn.net/qq_42739587/article/details/129674548