目录

用 GPT-4 给开源项目 GoPool 重构测试代码 - 每天5分钟玩转 GPT 编程系列(8)

1. 好险,差点被喷

早几天发了一篇文章:《仅三天,我用 GPT-4 生成了性能全网第一的 Golang Worker Pool,轻松打败 GitHub 万星项目》,这标题是挺容易被怼,哇咔咔;不过最终“下场”不算惨,怼的声音很少,相反很多开发者马上就在自己的项目里开始使用 GoPool了,刺激!

等等,你以为“刺激”的只是“我”?咳咳,其实,那些开发者也感觉很“刺激”。这不,他们被螃蟹壳塞牙了,你看 GoPool 的 issue 列表

/devchat-gpt-in-5min-8-ut/1.png

容我辩解5秒钟)Bugs 有归有,不过平均一个 issue 的解决时间没有超过半天,至少我提供的售后服务还可以的,对吧?哈哈……

5秒到了)不得不承认:一个足够健壮的 Worker Pool 还是需要花点时间来打磨。

那就再打磨一下吧:

  1. 解 bugs
  1. 完善测试用例

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 是一个配套的断言库。你可以通过以下命令安装它们:
1
2
go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega/...
  1. 然后,你需要在你的测试目录下生成一个新的 Ginkgo 测试套件。你可以通过以下命令生成:
1
ginkgo bootstrap

这将会生成一个名为 suite_test.go 的文件,其中包含了一个基本的测试套件骨架。

  1. 接下来,你可以为你的 gopool 包生成一个 Ginkgo 测试文件。你可以通过以下命令生成:
1
ginkgo generate gopool

这将会生成一个名为 gopool_test.go 的文件,其中包含了一个基本的测试文件骨架。

  1. 最后,你可以在 gopool_test.go 文件中添加你的测试用例。以下是一个基本的测试用例骨架:
1
2
3
4
5
6
7
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 中:

/devchat-gpt-in-5min-8-ut/2.png

2.2 尝试改造旧的测试用例

骨架有了,接下来我准备尝试让 GPT-4 来重写代码。

  • 我对 DevChat 说:

我将原先的测试用例到备份到了 gopool_1_test.go 中,现在我需要你用 Ginkgo 的最佳实践重构测试代码,新的代码将会放到 gopool_test.go 中。

DevChat 的回答如下:


首先,我们需要将原先的测试用例转换为 Ginkgo 的格式。这包括将每个测试用例转换为一个 It 块,并使用 Gomega 断言库来替换原先的断言。以下是转换后的测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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 的训练数据太旧导致它不知道这件事。(没错,这时候是它不是她。)

/devchat-gpt-in-5min-8-ut/3.png

于是我先“笑纳”了这段代码,然后拆解问题,暂且将压测代码放到单独的文件,让 GPT-4 安心把功能测试代码重构好,然后我再和它算“基准测试”的账。

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

  • gopool_benchmark_test.go

里面内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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-gpt-in-5min-8-ut/4.png

提示: DevChat 给出的代码库右上角有一个“diff”按钮,点一下可以进入“diff 视图”,这样就能看到当前打开文件中的代码和 GPT-4 给出的代码之间的 diff,然后你就能清晰地看到 GPT-4 给出的代码具体修改了什么了。

DevChat 的完整回答是:


好的,我将会按照你的要求,将 gopool_1_test.go 中的测试代码用 Ginkgo 的方式重构,并更新到 gopool_test.go 中。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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 的,不过运行的时候会遇到这个错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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 就变成了这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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 插件,圆满了:

/devchat-gpt-in-5min-8-ut/5.png

通过 ginkgo 插件运行一下:

/devchat-gpt-in-5min-8-ut/6.png

也可以在命令行里自己敲 ginkgo -v 感受下更详细的结果输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
$ 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 和度娘”。