我目前正在考虑在 Go 中为我的服务创建一些单元测试,以及在该功能之上构建的其他功能,我想知道在 Go 中进行单元测试的最佳方法是什么?我的代码如下所示:
type BBPeripheral struct {
client *http.Client
endpoint string
}
type BBQuery struct {
Name string `json:"name"`
}
type BBResponse struct {
Brand string `json:"brand"`
Model string `json:"model"`
...
}
type Peripheral struct {
Brand string
Model string
...
}
type Service interface {
Get(name string) (*Peripheral, error)
}
func NewBBPeripheral(config *peripheralConfig) (*BBPeripheral, error) {
transport, err := setTransport(config)
if err != nil {
return nil, err
}
BB := &BBPeripheral{
client: &http.Client{Transport: transport},
endpoint: config.Endpoint[0],
}
return BB, nil
}
func (this *BBPeripheral) Get(name string) (*Peripheral, error) {
data, err := json.Marshal(BBQuery{Name: name})
if err != nil {
return nil, fmt.Errorf("BBPeripheral.Get Marshal: %s", err)
}
resp, err := this.client.Post(this.endpoint, "application/json", bytes.NewBuffer(data))
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(resp.StatusCode)
}
var BBResponse BBResponse
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(body, &BBResponse)
if err != nil {
return nil, err
}
peripheral := &Peripheral{}
peripheral.Model = BBResponse.Model
if peripheral.Model == "" {
peripheral.Model = NA
}
peripheral.Brand = BBResponse.Brand
if peripheral.Brand == "" {
peripheral.Brand = NA
}
return peripheral, nil
}
测试此代码以及使用这些函数启动单独的 goroutine 以像服务器一样运行、使用 http.httptest 包或其他东西的代码是最有效的方法吗?这是我第一次尝试编写测试,但我真的不知道如何编写。
这完全取决于。 Go 提供了在每个级别测试应用程序所需的几乎所有工具。
单元测试
设计很重要,因为动态提供模拟/存根对象的技巧并不多。 您可以覆盖测试变量,但它可以解决清理方面的各种问题。 我会专注于 IO 免费单元测试来检查您的特定逻辑是否有效。
例如,您可以通过将
BBPeripheral.Get
创建为一个接口,在实例化期间需要它,并为测试提供一个存根来测试 client
方法。
func Test_BBPeripheral_Get_Success(*testing.T) {
bb := BBPeripheral{client: &StubSuccessClient, ...}
p, err := bb.Get(...)
if err != nil {
t.Fail()
}
}
然后您可以创建一个存根错误客户端,在
Get
方法中执行错误处理:
func Test_BBPeripheral_Get_Success(*testing.T) {
bb := BBPeripheral{client: &StubErrClient, ...}
_, err := bb.Get(...)
if err == nil {
t.Fail()
}
}
组件/集成测试
这些测试可以帮助锻炼软件包中的每个单独单元可以协同工作。 由于您的代码通过 http 进行通信,因此 Go 提供了可以使用的
httptest
包。
为此,测试可以创建一个 httptest 服务器,并注册一个处理程序以提供
this.endpoint
期望的响应。 然后,您可以通过请求 NewBBPeripheral
,传入与 this.endpoint
属性对应的
Server.URL
,使用其公共接口来练习代码。
这允许您模拟与真实服务器对话的代码。
进行常规测试
Go 使编写并发代码变得如此容易,并且使测试它也变得同样容易。 测试生成一个执行
NewBBPeripheral
的 go 例程的顶级代码可能看起来非常像上面的测试。 除了启动测试服务器之外,您的测试还必须等待异步代码完成。 如果您没有一种服务范围内的方法来取消/关闭/发出完成信号,那么可能需要使用 go 例程来测试它。
竞赛条件/负载测试
使用 go 内置的基准测试与
-race
标志相结合,您可以轻松地练习您的代码,并利用您上面编写的测试来分析它的竞争条件。
要记住的一件事是,如果应用程序的实现仍在不断变化,则编写单元测试可能会花费大量时间。 创建几个测试来执行代码的公共接口,应该可以让您轻松验证应用程序是否正常工作,同时允许更改实现。
Go 让编写并发代码变得如此简单,也让测试它变得同样容易。
testing/synctest
:用于测试并发代码的新包”,在 2024-08-27 Go 编译器和运行时会议笔记中提到
这个包有两个主要特点:
- 它允许使用假时钟来测试使用计时器的代码。测试可以控制被测试代码观察到的时间流逝。
- 它允许测试等待异步操作完成。
我们可以注入一个假时钟来控制测试中的时间。当推进假时钟时,我们需要某种机制来确保在进行测试之前所有触发的计时器都已执行。这些更改是以额外的代码复杂性为代价的:我们不能再使用
,但必须使用可测试的包装器。后台 goroutine 需要额外的同步点。time.Timer
示例:
func TestCacheEntryExpires(t *testing.T) { synctest.Run(func() { count := 0 c := NewCache(2 * time.Second, func(key string) int { count++ return fmt.Sprintf("%v:%v", key, count) }) // Get an entry from the cache. if got, want := c.Get("k"), "k:1"; got != want { t.Errorf("c.Get(k) = %q, want %q", got, want) } // Verify that we get the same entry when accessing it before the expiry. time.Sleep(1 * time.Second) synctest.Wait() if got, want := c.Get("k"), "k:1"; got != want { t.Errorf("c.Get(k) = %q, want %q", got, want) } // Wait for the entry to expire and verify that we now get a new one. time.Sleep(3 * time.Second) synctest.Wait() if got, want := c.Get("k"), "k:2"; got != want { t.Errorf("c.Get(k) = %q, want %q", got, want) } }) }
这与上面的简单测试相同,包含在
中,并添加了两次对synctest.Run
的调用。synctest.Wait
然而:
- 这个测试并不慢。 time.Sleep 调用使用假时钟,并立即执行。
- 这个测试并不不稳定。
确保所有后台 goroutine 在测试继续之前已空闲或退出。synctest.Wait
- 此测试不需要对被测代码进行额外的检测。它可以使用标准时间包定时器,并且不需要提供任何机制让测试与之同步。