ChatGPT は単体テストの練習を生成します (Golang)

序文

現時点では、gpt は本質的に継続であるため、テストする関数が明確に定義されていれば、それに基づいて単体テストを適切に生成できます。

メリットは何ですか:

  1. テストケースとテストコードの生成を支援し、単体テスト作成の精神的コストを削減します。
  2. コードの明示的/潜在的な問題の発見に役立つ補助コードレビュー

この記事のテスト環境は次のとおりです。

  • gpt: gpt-3.5-ターボ
  • 行く:行く 1.17

この記事の実践シナリオ: Enterprise WeChat Meitu Appreciation Robot


単体テストを生成するワークフローは次のとおりです。

  1. テストする機能を選択してください
  2. 関数の依存関係の分析: 構造定義、リポジトリの依存関係、リポジトリ インターフェイスの抽象化 (fx や Wire などの依存関係注入フレームワークを使用するとはるかに便利です)
  3. 組織のプロンプト、呪文を唱える準備ができました
  4. 魔法を歌って、出力単体テスト コードを取得します
  5. 縫製と修復のために IDE にコピーします。期待との相違が多すぎる場合は、プロンプトを調整し、再起動するか、特定の基準に従って gpt を書き換えるように要求します (例: testCases メソッドに従って組織のテスト データを書き換えてください)
  6. テストケースを実行する

言うことはあまりありません。基本的なプロンプトから始めましょう。

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

次の部分では、ワークフローの複雑さに応じて、簡単/通常/困難な状況を簡単に区別します。


パート 1 簡単: 単一関数、複雑な依存関係なし

単一関数と外部依存関係はすべてオープン ソース ライブラリなので、コードを直接貼り付けるだけです。

ここでは、例として画像圧縮の独立した機能を使用します:
(プロンプト フロント シェイク マントラ + コード スニペット)

写下列代码中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 通常: 内部に外部インポートがいくつかあります

外部定義を導入する場合は、外部定義を直接貼り付けることをお勧めします (モックは使用しません)。

ここで例に挙げた関数には、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)
      })
   }
}

マイナー修正後はスムーズに動作


パート 3 ハード: 外部リポジトリのモック (gomock の例)


ps. 実際、gomonkey は
外部依存関係リポジトリの一部を書き込むこともでき、gpt が gomock を使用して対応するリポジトリをモックする必要があります
外側の定義は削除するのが最適です。

入力:

写下列代码中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 コードは正常に生成できますが、外部リポジトリの定義は明確である必要があり、インターフェイス定義を直接貼り付けると認識されます。
  2. これを IDE に貼り付けると、主な問題はパッケージ名のインポートであることがわかりました。パッケージ名は簡単に変更できます。

生成されたコードにはいくつかのエラーがあります。
何らかのエラー

インポートされた型名を変更した後:
軽微な修理

変更後は正常に実行されます。
走る!

生成後の人為的変更の量を少なくする必要がある場合は、より正確なコンテキスト情報 (パッケージ名、関数定義、説明など) が必要です。


いくつかの問題点

  1. ChatGPT には 1 つの会話の長さに制限があるため、一度に 1 つの関数に対してのみ単体テストを生成するようにしてください。
  2. 外部リポジトリのモックを必要とするテスト対象の関数には多くのコンテキストが必要であり、入力の汚染を避けるために導入された無関係なフィールドを削除する必要があります。
  3. 一部の複雑なプライベートパッケージはサポートが難しいようです(プロンプトで教えようとしましたが、効果は良くありません)
  4. 一部の複雑なロジックや境界線の状況では、AI は疑わしい場合があります (書かれたユースケースは、古典的な 20%*20%=400% などの式と一致しませんが、AI はそれが間違っていることを知っています)。
  5. レイヤーが明確ではないプロジェクトの場合、関数/リポジトリを積み上げるのが難しい場合があります。現時点では、明確に記述するのがさらに困難です。gomonkey の方が適しているかもしれません。

その他の用途

  1. 適切なテストメソッドを作成し、gpt を利用してユースケースセットを拡張しましょう
  2. コードレビュー。次のコードは 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 和可能的错误,避免了空值的返回。最后,增加错误检查,保证代码健壮性。

おすすめ

転載: blog.csdn.net/qq_42739587/article/details/129674548
おすすめ