前语
目前gpt本质上是续写,所以在待测函数函数界说明晰的状况下,单元测验能够适当依靠它进行生成。
收益是什么:
- 辅佐生成测验用例&测验代码,下降单元测验编写的心智成本
- 辅佐code review,帮助发现代码显式/潜在问题
本文测验环境:
- gpt: gpt-3.5-turbo
- go:go 1.17
本文实践场景:企业微信美图鉴赏机器人
生成单元测验的工作流如下:
- 选定你的待测函数
- 分析函数的依靠:结构体界说、repo依靠、repo interface笼统(用fx和wire框架依靠注入的话会便利许多)
- 安排prompt,准备施法
- 吟唱魔法,得到输出单元测验代码
- copy到ide里边缝缝补补,与预期出入太多的话,调整prompt重来/要求gpt依照某个规范重写(比如:请依照testCases的写法重写安排测验数据)
- 运行测验用例
话不多说,先上根底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里,完美运行,说明简略函数的单元测验生成还是很强的~
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: "",
},
},
Data: []byte{},
},
expected: &entity.BotMsgReq{
MsgType: entity.BotMsgText,
Text: &entity.BotText{
Content: "proxy图源:\npixiv图源:https://www.pixiv.net/artworks/123456",
},
},
},
{
name: "data with special characters",
data: &entity.ArchiveWithData{
Info: entity.Archive{
Pid: 789012,
Urls: entity.PicUrl{
Original: "",
},
},
Data: []byte{},
},
expected: &entity.BotMsgReq{
MsgType: entity.BotMsgText,
Text: &entity.BotText{
Content: "proxy图源:\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",期望返回过错信息。
生成的代码太长,几点总结:
- gomock代码能够正常生成,可是外部repo界说最好明晰明了,能够直接把interface界说贴上去,会被认知到。
- 粘贴到ide中,发现首要问题是包名导入问题,简略修正即可
生成的代码有一些报错:
修正引进的类型名后即可:
修正后运行成功:
假如需求生成后人工修正量更低的话,需求更准确的上下文信息(包名、函数界说、描绘等)
一些痛点
- ChatGPT单次对话有长度限制,所以一次尽量只进行一个函数的单元测验生成。
- 需求外部 repo mock的待测函数,需求的上下文许多,并且引进的无关字段需求剪枝防止污染输入
- 一些杂乱的私有包,看起来是比较欠好支撑的(测验过在prompt中教会他,可是作用欠好)
- 一些杂乱逻辑和鸿沟状况,ai或许言不由衷(写的用例和表述不一致,比如经典的20%*20%=400%,可是他知道这是错的)
- 对于分层不是很明晰的项目,对函数/repo打桩或许很困难,这个时分要描绘清楚就比较困难了,gomonkey或许比较适用
其他用法
- 写好测验办法,让gpt帮助扩大用例集
- 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 和或许的过错,防止了空值的返回。最后,添加过错检查,确保代码健壮性。