ChatGPT生成单元测试实践(Golang)

Table of Contents

前言

目前gpt本质上是续写,所以在待测函数定义清晰的情况下,单元测试可以适当依赖它进行生成。

收益是什么:

  1. 辅助生成测试用例&测试代码,降低单元测试编写的心智成本
  2. 辅助code review,帮助发现代码显式/潜在问题

本文测试环境:

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

本文实践场景:企业微信美图鉴赏机器人

生成单元测试的工作流如下:

  1. 选定你的待测函数
  2. 分析函数的依赖:结构体定义、repo依赖、repo interface抽象(用fx或者wire等依赖注入框架的话会方便很多)
  3. 组织prompt,准备施法
  4. 吟唱魔法,得到输出单元测试代码
  5. copy到ide里面缝缝补补,与预期出入太多的话,调整prompt重来/要求gpt按照某个标准重写(比如:请按照testCases的写法重写组织测试数据)
  6. 运行测试用例

话不多说,先上基础prompt:

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

下面的部分,我按照工作流的繁杂程度,简单区分了下easy/normal/hard情况。

Part1 easy:单个函数,无复杂依赖

单个函数,并且外部依赖都是开源库,那么直接把代码贴上去就行。

这里我们用图片压缩的独立函数举例:
(prompt前摇咒语 + 代码片段)

写下列代码中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
}

ChatGPT输出的代码块如下:

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)
      })

   }
}

放到ide里,完美运行,说明简单函数的单元测试生成还是很强的~

pass

Part2 normal :里面有一些外部import

有外部定义引入的时候,最好把外部定义直接贴上(不涉及mock)。

这里举例的函数出现了外部依赖 entity.ArchiveWithData等定义,所以最好要加上外部定义在输入中:

写下列代码中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
}

下面是生成的代码,经过少量修改后可以直接运行:
(因为少输入了一个结构体的定义,导致它猜测了一些字段)

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)
      })
   }
}

小修小补之后顺利运行

Part3 hard:对外部repo进行mock(gomock举例)

外部依赖越多,prompt template的构建可能就越复杂
ps.实际上gomonkey它也可以写
外部依赖repo的部分,要求gpt使用gomock进行对应repo的mock即可。
外部定义最好进行剪枝。

输入:

写下列代码中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
}

输出:

假设已经有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",期望返回错误信息。

生成的代码太长,几点总结:

  1. gomock代码可以正常生成,但是外部repo定义最好清晰明了,可以直接把interface定义贴上去,会被认知到。
  2. 粘贴到ide中,发现主要问题是包名导入问题,简单修改即可

生成的代码有一些报错:
有些报错

修改引入的类型名后即可:
小修小补

修改后运行成功:
run!

如果需要生成后人工修改量更低的话,需要更精确的上下文信息(包名、函数定义、描述等)

一些痛点

  1. ChatGPT单次对话有长度限制,所以一次尽量只进行一个函数的单元测试生成。
  2. 需要外部 repo mock的待测函数,需要的上下文很多,并且引入的无关字段需要剪枝避免污染输入
  3. 一些复杂的私有包,看起来是比较不好支持的(尝试过在prompt中教会他,但是效果不好)
  4. 一些复杂逻辑和边界情况,ai可能口是心非(写的用例和表述不一致,比如经典的20%*20%=400%,但是他知道这是错的)
  5. 对于分层不是很清晰的项目,对函数/repo打桩可能很困难,这个时候要描述清楚就比较困难了,gomonkey可能比较适用

其他用法

  1. 写好测试方法,让gpt帮助扩充用例集
  2. code review,下面的代码就是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 和可能的错误,避免了空值的返回。最后,增加错误检查,保证代码健壮性。

文章出处登录后可见!

立即登录
已经登录? 立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
社会演员多的头像社会演员多普通用户
0
5.1统计英文文件中的单词数python
上一篇 2023年6月13日
Python 导入csv数据的三种方法分享
下一篇 2023年6月13日

相关推荐

  • yolov5 backbone 更改为 mobilevit(即改即用) 2023年8月8日
  • 香橙派5使用NPU加速yolov5的实时视频推理(一) 2023年4月19日
  • OpenGL相机自由移动旋转缩放,四元数,欧拉角,LookAt 2023年4月5日
  • Cartographer算法2D激光雷达与IMU融合建图 2023年3月4日
  • yolov5+deepsort实现实时键盘自动切换单目标跟踪 2022年5月20日
  • 面试成功后如何谈薪资? 2023年12月14日
  • 机器学习实战教程汇总(十三篇) 2023年3月15日
  • 零基础,零成本,部署一个属于你的大模型 2023年5月30日
  • python用哪个版本比较好,python什么版本比较稳定 2023年11月13日
  • 【周末闲谈】人工智能热潮下的AIGC到底指的是什么? 2023年8月17日
  • 【深度学习】目标检测的性能评价指标,mAP_0.5,mAP_0.5,0.95,0.05 2023年2月25日
  • 【目标检测实战学习】数据增强的几种方法:cutout,mixup,mosaic,rotate,HSV,随机抖动实战 2023年3月28日
  • 海康工业相机 C# 开发示例 2023年2月24日
  • torch.nn.Conv3d 2023年3月4日
  • 时序分析 49 — 贝叶斯时序预测(一) 2023年6月5日
  • 文心一言硬刚ChatGPT。文心一言能否为百度止颓?中国版ChatGPT“狂飙”的机会在哪儿? 2023年4月5日

赞助商

此站出售,如需请站内私信或者邮箱!

玻璃钢生产厂家卢湾玻璃钢雕塑工程玻璃钢雕塑易燃吗重庆商场节庆美陈雕塑报价玻璃钢雕塑销售的主要对象晋城玻璃钢海豚雕塑价格玻璃钢卡通唐老鸭雕塑哈尔滨户外玻璃钢雕塑防城港玻璃钢泡沫雕塑公司商场美陈夏玻璃钢雕塑摆放方式温州玻璃钢人物雕塑厂家玻璃钢主题雕塑制作南汇玻璃钢音符雕塑沈阳阳江玻璃钢卡通雕塑西宁仿真玻璃钢雕塑多少钱商场软装设计美陈装修香港商场美陈费用佛山玻璃钢花盆批发价格济源景观玻璃钢雕塑泾县玻璃钢雕塑厂家玻璃钢马雕塑制作商场灯光设施美陈合肥卡通玻璃钢雕塑生产厂家九江创意玻璃钢雕塑生产厂家玻璃钢仿真水果雕塑单价玻璃钢瓜果雕塑生产厂耐腐蚀玻璃钢雕塑制造厂家北京蛋形玻璃钢花盆泰州玻璃钢人物雕塑甘孜玻璃钢雕塑定制香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警汪小菲曝离婚始末遭遇山火的松茸之乡雅江山火三名扑火人员牺牲系谣言何赛飞追着代拍打萧美琴窜访捷克 外交部回应卫健委通报少年有偿捐血浆16次猝死手机成瘾是影响睡眠质量重要因素高校汽车撞人致3死16伤 司机系学生315晚会后胖东来又人满为患了小米汽车超级工厂正式揭幕中国拥有亿元资产的家庭达13.3万户周杰伦一审败诉网易男孩8年未见母亲被告知被遗忘许家印被限制高消费饲养员用铁锨驱打大熊猫被辞退男子被猫抓伤后确诊“猫抓病”特朗普无法缴纳4.54亿美元罚金倪萍分享减重40斤方法联合利华开始重组张家界的山上“长”满了韩国人?张立群任西安交通大学校长杨倩无缘巴黎奥运“重生之我在北大当嫡校长”黑马情侣提车了专访95后高颜值猪保姆考生莫言也上北大硕士复试名单了网友洛杉矶偶遇贾玲专家建议不必谈骨泥色变沉迷短剧的人就像掉进了杀猪盘奥巴马现身唐宁街 黑色着装引猜测七年后宇文玥被薅头发捞上岸事业单位女子向同事水杯投不明物质凯特王妃现身!外出购物视频曝光河南驻马店通报西平中学跳楼事件王树国卸任西安交大校长 师生送别恒大被罚41.75亿到底怎么缴男子被流浪猫绊倒 投喂者赔24万房客欠租失踪 房东直发愁西双版纳热带植物园回应蜉蝣大爆发钱人豪晒法院裁定实锤抄袭外国人感慨凌晨的中国很安全胖东来员工每周单休无小长假白宫:哈马斯三号人物被杀测试车高速逃费 小米:已补缴老人退休金被冒领16年 金额超20万

玻璃钢生产厂家 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化