用 GPT-4 給開源項目 GoPool 重構測試代碼 - 每天5分鐘玩轉 GPT 編程系列(8)

来源:https://www.cnblogs.com/daniel-hutao/archive/2023/08/16/devchat-gpt-in-5min-8-ut.html
-Advertisement-
Play Games

[TOC] ## 1. 好險,差點被噴 早幾天發了一篇文章:[《僅三天,我用 GPT-4 生成了性能全網第一的 Golang Worker Pool,輕鬆打敗 GitHub 萬星項目》](https://www.danielhu.cn/golang-gopool-1/),這標題是挺容易被懟,哇咔咔; ...


目錄

1. 好險,差點被噴

早幾天發了一篇文章:《僅三天,我用 GPT-4 生成了性能全網第一的 Golang Worker Pool,輕鬆打敗 GitHub 萬星項目》,這標題是挺容易被懟,哇咔咔;不過最終“下場”不算慘,懟的聲音很少,相反很多開發者馬上就在自己的項目里開始使用 GoPool了,刺激!

等等,你以為“刺激”的只是“我”?咳咳,其實,那些開發者也感覺很“刺激”。這不,他們被螃蟹殼塞牙了,你看 GoPool 的 issue 列表

容我辯解5秒鐘)Bugs 有歸有,不過平均一個 issue 的解決時間沒有超過半天,至少我提供的售後服務還可以的,對吧?哈哈……

5秒到了)不得不承認:一個足夠健壯的 Worker Pool 還是需要花點時間來打磨。

那就再打磨一下吧:

2. 重構測試代碼

當前所有測試用例都放在 gopool_test.go 中,這個文件已經接近200行了,維護起來開始吃力。所以我決定今天先讓 GPT-4 來改造一下測試用例的組織方式。

下文使用 DevChat 和 GPT-4 交互。如果你對 DevChat 感興趣,可以跳轉閱讀這篇文章:

2.1 引入 Ginkgo 測試框架

  • 我對 DevChat 說:

當前我的測試代碼全部在 gopool_test.go 中,我覺得可讀性不夠好。我希望你引入 Ginkgo 測試框架來幫我重新組織測試代碼。首先我只需要你告訴我如何引入 Ginkgo,搭建測試骨架代碼,而不用編寫具體的測試用例。

DevChat 的回答如下:


要引入 Ginkgo 測試框架並搭建測試骨架代碼,你需要按照以下步驟進行:

  1. 首先,你需要安裝 Ginkgo 和 Gomega。Ginkgo 是一個 Go 測試框架,Gomega 是一個配套的斷言庫。你可以通過以下命令安裝它們:
go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega/...
  1. 然後,你需要在你的測試目錄下生成一個新的 Ginkgo 測試套件。你可以通過以下命令生成:
ginkgo bootstrap

這將會生成一個名為 suite_test.go 的文件,其中包含了一個基本的測試套件骨架。

  1. 接下來,你可以為你的 gopool 包生成一個 Ginkgo 測試文件。你可以通過以下命令生成:
ginkgo generate gopool

這將會生成一個名為 gopool_test.go 的文件,其中包含了一個基本的測試文件骨架。

  1. 最後,你可以在 gopool_test.go 文件中添加你的測試用例。以下是一個基本的測試用例骨架:
var _ = Describe("Gopool", func() {
    Context("when something happens", func() {
        It("should do something", func() {
            // Your test code goes here
        })
    })
})

在這個骨架中,Describe 用於描述你正在測試的對象,Context 用於描述你正在測試的場景,It 用於描述你的期望結果。

以上就是引入 Ginkgo 並搭建測試骨架代碼的步驟。你可以根據你的需求修改和添加測試用例。


因為 gopool_test.go 文件已經存在,Ginkgo 也沒有智能到能夠直接“merge”,所以我先將當前代碼備份到了 gopool_1_test.go 中:

2.2 嘗試改造舊的測試用例

骨架有了,接下來我準備嘗試讓 GPT-4 來重寫代碼。

  • 我對 DevChat 說:

我將原先的測試用例到備份到了 gopool_1_test.go 中,現在我需要你用 Ginkgo 的最佳實踐重構測試代碼,新的代碼將會放到 gopool_test.go 中。

DevChat 的回答如下:


首先,我們需要將原先的測試用例轉換為 Ginkgo 的格式。這包括將每個測試用例轉換為一個 It 塊,並使用 Gomega 斷言庫來替換原先的斷言。以下是轉換後的測試代碼:

package gopool_test

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"

	"github.com/devchat-ai/gopool"
)

var _ = Describe("Gopool", func() {
	Describe("With Mutex", func() {
		It("should work correctly", func() {
			// Your test code goes here
		})
	})

	Describe("With SpinLock", func() {
		It("should work correctly", func() {
			// Your test code goes here
		})
	})

	Describe("With Error", func() {
		It("should work correctly", func() {
			// Your test code goes here
		})
	})

	Describe("With Result", func() {
		It("should work correctly", func() {
			// Your test code goes here
		})
	})

	Describe("With Retry", func() {
		It("should work correctly", func() {
			// Your test code goes here
		})
	})

	Describe("With Timeout", func() {
		It("should work correctly", func() {
			// Your test code goes here
		})
	})

	Describe("With MinWorkers", func() {
		It("should work correctly", func() {
			// Your test code goes here
		})
	})
})

這隻是一個骨架代碼,你需要將原先的測試代碼複製到對應的 It 塊中,並使用 Gomega 斷言庫來替換原先的斷言。例如,你可以將 t.Errorf 替換為 Expect().To(Equal())

對於基準測試,Ginkgo 並不直接支持,但你可以使用 Go 的原生 testing 包來進行基準測試。你可以將基準測試代碼放在一個單獨的文件中,例如 gopool_benchmark_test.go


啥?我感覺 GPT-4 偷懶了,居然不直接給我把代碼寫好。另外我沒記錯的話,Ginkgo 是支持“基準測試”的,可能是 GPT-4 的訓練數據太舊導致它不知道這件事。(沒錯,這時候是它不是她。)

於是我先“笑納”了這段代碼,然後拆解問題,暫且將壓測代碼放到單獨的文件,讓 GPT-4 安心把功能測試代碼重構好,然後我再和它算“基準測試”的賬。

此刻我就有了一個新文件:

  • gopool_benchmark_test.go

裡面內容如下:

package gopool

import (
	"sync"
	"testing"
	"time"

	"github.com/daniel-hutao/spinlock"
)



func BenchmarkGoPoolWithMutex(b *testing.B) {
	var wg sync.WaitGroup
	var taskNum = int(1e6)
	pool := NewGoPool(1e4, WithLock(new(sync.Mutex)))
	defer pool.Release()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		wg.Add(taskNum)
		for num := 0; num < taskNum; num++ {
			pool.AddTask(func() (interface{}, error) {
				time.Sleep(10 * time.Millisecond)
				wg.Done()
				return nil, nil
			})
		}
		wg.Wait()
	}
	b.StopTimer()
}

func BenchmarkGoPoolWithSpinLock(b *testing.B) {
	var wg sync.WaitGroup
	var taskNum = int(1e6)
	pool := NewGoPool(1e4, WithLock(new(spinlock.SpinLock)))
	defer pool.Release()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		wg.Add(taskNum)
		for num := 0; num < taskNum; num++ {
			pool.AddTask(func() (interface{}, error) {
				time.Sleep(10 * time.Millisecond)
				wg.Done()
				return nil, nil
			})
		}
		wg.Wait()
	}
	b.StopTimer()
}

func BenchmarkGoroutines(b *testing.B) {
	var wg sync.WaitGroup
	var taskNum = int(1e6)

	for i := 0; i < b.N; i++ {
		wg.Add(taskNum)
		for num := 0; num < taskNum; num++ {
			go func() (interface{}, error) {
				time.Sleep(10 * time.Millisecond)
				wg.Done()
				return nil, nil
			}()
		}
		wg.Wait()
	}
}

gopool_1_test.go 的內容我就不贅述了,就是原先的全量測試代碼減去上面這些基準測試代碼。那麼接下來問題就拆分成了2個:

  1. 用 Ginkgo 的方式改造功能測試代碼
  2. 用 Ginkgo 的方式改造基準測試代碼

不過目測本文篇幅有點長了,所以我決定今天先完成功能測試代碼的改造,基準測試下次再說。

2.3 重構功能測試代碼

接下來該讓 GPT-4 開始重構功能測試代碼了。

  • 我對 DevChat 說:

幫我將 gopool_1_test.go 中的用 testing 庫編寫的測試代碼全部用 Ginkgo 的方式重構,更新到 gopool_test.go 中。

提示: DevChat 給出的代碼庫右上角有一個“diff”按鈕,點一下可以進入“diff 視圖”,這樣就能看到當前打開文件中的代碼和 GPT-4 給出的代碼之間的 diff,然後你就能清晰地看到 GPT-4 給出的代碼具體修改了什麼了。

DevChat 的完整回答是:


好的,我將會按照你的要求,將 gopool_1_test.go 中的測試代碼用 Ginkgo 的方式重構,並更新到 gopool_test.go 中。

package gopool_test

import (
	"errors"
	"sync"
	"sync/atomic"
	"time"

	"github.com/daniel-hutao/spinlock"
	"github.com/devchat-ai/gopool"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("Gopool", func() {
	Describe("With Mutex", func() {
		It("should work correctly", func() {
			pool := gopool.NewGoPool(100, gopool.WithLock(new(sync.Mutex)))
			defer pool.Release()
			for i := 0; i < 1000; i++ {
				pool.AddTask(func() (interface{}, error) {
					time.Sleep(10 * time.Millisecond)
					return nil, nil
				})
			}
			pool.Wait()
		})
	})

	Describe("With SpinLock", func() {
		It("should work correctly", func() {
			pool := gopool.NewGoPool(100, gopool.WithLock(new(spinlock.SpinLock)))
			defer pool.Release()
			for i := 0; i < 1000; i++ {
				pool.AddTask(func() (interface{}, error) {
					time.Sleep(10 * time.Millisecond)
					return nil, nil
				})
			}
			pool.Wait()
		})
	})

	Describe("With Error", func() {
		It("should work correctly", func() {
			var errTaskError = errors.New("task error")
			pool := gopool.NewGoPool(100, gopool.WithErrorCallback(func(err error) {
				Expect(err).To(Equal(errTaskError))
			}))
			defer pool.Release()

			for i := 0; i < 1000; i++ {
				pool.AddTask(func() (interface{}, error) {
					return nil, errTaskError
				})
			}
			pool.Wait()
		})
	})

	Describe("With Result", func() {
		It("should work correctly", func() {
			var expectedResult = "task result"
			pool := gopool.NewGoPool(100, gopool.WithResultCallback(func(result interface{}) {
				Expect(result).To(Equal(expectedResult))
			}))
			defer pool.Release()

			for i := 0; i < 1000; i++ {
				pool.AddTask(func() (interface{}, error) {
					return expectedResult, nil
				})
			}
			pool.Wait()
		})
	})

	Describe("With Retry", func() {
		It("should work correctly", func() {
			var retryCount = int32(3)
			var taskError = errors.New("task error")
			var taskRunCount int32 = 0

			pool := gopool.NewGoPool(100, gopool.WithRetryCount(int(retryCount)))
			defer pool.Release()

			pool.AddTask(func() (interface{}, error) {
				atomic.AddInt32(&taskRunCount, 1)
				if taskRunCount <= retryCount {
					return nil, taskError
				}
				return nil, nil
			})

			pool.Wait()

			Expect(atomic.LoadInt32(&taskRunCount)).To(Equal(retryCount + 1))
		})
	})

	Describe("With Timeout", func() {
		It("should work correctly", func() {
			var taskRun int32

			pool := gopool.NewGoPool(100, gopool.WithTimeout(100*time.Millisecond), gopool.WithErrorCallback(func(err error) {
				Expect(err.Error()).To(Equal("task timed out"))
				atomic.StoreInt32(&taskRun, 1)
			}))
			defer pool.Release()

			pool.AddTask(func() (interface{}, error) {
				time.Sleep(200 * time.Millisecond)
				return nil, nil
			})

			pool.Wait()

			Expect(atomic.LoadInt32(&taskRun)).To(Equal(int32(1)))
		})
	})

	Describe("With MinWorkers", func() {
		It("should work correctly", func() {
			var minWorkers = 50

			pool := gopool.NewGoPool(100, gopool.WithMinWorkers(minWorkers))
			defer pool.Release()

			Expect(pool.GetWorkerCount()).To(Equal(minWorkers))
		})
	})
})

這段代碼將 gopool_1_test.go 中的測試用例轉換為 Ginkgo 的格式,並使用 Gomega 斷言庫來替換原先的斷言。


這段代碼基本是 ok 的,不過運行的時候會遇到這個錯誤:

go test ./...
/var/folders/gv/f94q_vyx2m17d_36sf0zkm0r0000gn/T/go-build3608449998/b001/gopool.test flag redefined: ginkgo.seed
panic: /var/folders/gv/f94q_vyx2m17d_36sf0zkm0r0000gn/T/go-build3608449998/b001/gopool.test flag redefined: ginkgo.seed

goroutine 1 [running]:
flag.(*FlagSet).Var(0x14000114120, {0x1049bd108, 0x104c0d4a0}, {0x1400011af70, 0xb}, {0x10486d229, 0x2a})
        /usr/local/go/src/flag/flag.go:982 +0x2a4
flag.(*FlagSet).Int64Var(...)
        /usr/local/go/src/flag/flag.go:769
github.com/onsi/ginkgo/config.Flags(0x14000191ba8?, {0x10485657a?, 0x989680?}, 0x1)
        /Users/danielhu/go/pkg/mod/github.com/onsi/[email protected]/config/config.go:75 +0xe0
github.com/onsi/ginkgo.init.0()
        /Users/danielhu/go/pkg/mod/github.com/onsi/[email protected]/ginkgo_dsl.go:53 +0x38
FAIL    github.com/devchat-ai/gopool    3.889s
FAIL

這時候 Google 一下,可以找到 Ginkgo issue 875。改起來也很容易,在 gopool_test.go 的開頭應該將 . "github.com/onsi/ginkgo" 改成 . "github.com/onsi/ginkgo/v2"

所以目前 go.mod 就變成了這樣:

module github.com/devchat-ai/gopool

go 1.20

require (
	github.com/daniel-hutao/spinlock v0.1.0
	github.com/onsi/ginkgo/v2 v2.11.0
	github.com/onsi/gomega v1.27.10
)

require (
	github.com/go-logr/logr v1.2.4 // indirect
	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
	github.com/google/go-cmp v0.5.9 // indirect
	github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
	golang.org/x/net v0.12.0 // indirect
	golang.org/x/sys v0.10.0 // indirect
	golang.org/x/text v0.11.0 // indirect
	golang.org/x/tools v0.9.3 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

再裝一個 VS Code 的 ginkgo 插件,圓滿了:

通過 ginkgo 插件運行一下:

也可以在命令行里自己敲 ginkgo -v 感受下更詳細的結果輸出:

$ ginkgo -v
Running Suite: Gopool Suite - /Users/danielhu/go/mycode/gopool
==============================================================
Random Seed: 1692081153

Will run 7 of 7 specs
------------------------------
Gopool With Mutex should work correctly
/Users/danielhu/go/mycode/gopool/gopool_test.go:17
• [0.203 seconds]
------------------------------
Gopool With SpinLock should work correctly
/Users/danielhu/go/mycode/gopool/gopool_test.go:31
• [0.201 seconds]
------------------------------
Gopool With Error should work correctly
/Users/danielhu/go/mycode/gopool/gopool_test.go:45
• [0.102 seconds]
------------------------------
Gopool With Result should work correctly
/Users/danielhu/go/mycode/gopool/gopool_test.go:62
• [0.102 seconds]
------------------------------
Gopool With Retry should work correctly
/Users/danielhu/go/mycode/gopool/gopool_test.go:79
• [0.101 seconds]
------------------------------
Gopool With Timeout should work correctly
/Users/danielhu/go/mycode/gopool/gopool_test.go:102
• [0.202 seconds]
------------------------------
Gopool With MinWorkers should work correctly
/Users/danielhu/go/mycode/gopool/gopool_test.go:123
• [0.001 seconds]
------------------------------

Ran 7 of 7 Specs in 0.916 seconds
SUCCESS! -- 7 Passed | 0 Failed | 0 Pending | 0 Skipped
PASS

Ginkgo ran 1 suite in 4.759009833s
Test Suite Passed

到現在,測試相關的文件就有了3個:

  • gopool_benchmark_test.go
  • gopool_suite_test.go
  • gopool_test.go

到此,提個 PR :Refactor tests using Ginkgo and Gomega

3. 總結

因為 GPT-4 的訓練數據是大約2年前的,也就是最近2年這個世界發生了啥它是不知道的。所以對於一些變化大的庫,對於一些版本敏感的問題,你要對 GPT 給出的代碼保持警惕。有時候你需要的代碼對版本不敏感,那無所謂;反之,及時 Google 一下。

總之,擅用 GPT,但別隻用 GPT。偶爾還是想想你的老相好“Google Bing 和度娘”。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • —————— BEGIN —————— 1、測試對象條件 作為測試對象的醫院信息平臺(或系統)必須具備軟體著作權證書,運行一年以上並通過初驗。 2、標準符合性測試內容包括 3 部分 數據集標準符合性測試 依據標準 WS445-2014、WS 375.9-2012、WS 376.1-2013 的要求, ...
  • 1.3 分層架構演進 1.3.1 傳統四層架構 將領域模型和業務邏輯分離出來,並減少對基礎設施、用戶界面甚至應用層邏輯的依賴,因為它們不屬業務邏輯。將一個夏雜的系統分為不同的層,每層都應該具有良好的內聚性,並且只依賴於比其自身更低的層。 傳統分層架構的基礎設施層位於底層,持久化和消息機制便位於該層。 ...
  • ### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 一個應用同時連接多個資料庫進行操作,這是 ...
  • 背景: 上文學習了shrio 基本概念後,本章將進一步的落地實踐學習,在springboot中如何去整合shrio,整個過程步驟有個清晰的瞭解。 利用Shiro進行登錄認證主要步驟: 1. 添加依賴:首先,在pom.xml文件中添加Spring Boot和Shiro的相關依賴。 <!-- Sprin ...
  • ```java package com.java3y.austin.test; abstract class A{ public abstract void test(); } class B extends A{ B(){ System.out.println("B的構造函數"); } @Over ...
  • ## String的三種初始化方式 ```java public class Test { public static void main(String[] args) { String str1 = "Hello, World !"; //直接初始化 String str2 = new Strin ...
  • package com.yc.cloud.excel.util; import cn.hutool.poi.excel.ExcelWriter; import lombok.extern.slf4j.Slf4j; import org.apache.poi.hssf.usermodel.HSSFCl ...
  • ## 1、說明 一般情況下,都是在model中指定一個資料庫連接參數即可。但某些情況下,相同的庫表會在不同地區都有部署,這個時候需要按地區進行切換(只有一個model情況下)。 ## 2、多model繼承方式 Model層代碼 ``` //A地區的資料庫 class A extends Model ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...