22、Go语言代码测试全解析

Go语言代码测试全解析

1. 测试函数基础

测试函数的源代码通常会设定一个预期值,该值是基于对被测试代码的了解预先确定的。然后将这个预期值与被测试代码返回的计算值进行比较。例如,在进行两个向量相加时,我们可以使用向量加法规则计算预期结果,示例代码如下:

v1 := New(8.218, -9.341)
v2 := New(-1.129, 2.111)
v3 := v1.Add(v2)
expect := New(
    v1[0]+v2[0],
    v1[1]+v2[1],
)

在上述代码中,使用两个简单的向量值 v1 v2 计算出预期值,并将其存储在变量 expect 中。而变量 v3 则存储了被测试代码计算出的向量实际值。通过以下代码可以进行实际值与预期值的比较:

if !v3.Eq(expect) {
    t.Log("Addition failed, expecting %s, got %s", expect, v3)
    t.Fail()
}

如果测试条件为 false ,则表示测试失败,代码使用 t.Fail() 来标记测试函数失败。

2. 运行测试

测试函数可以使用 go test 命令行工具来执行。例如,在 vector 包中运行以下命令,将自动运行该包中的所有测试函数:

$> cd vector
$> go test .

也可以通过指定子包(或使用包通配符 ./... )来执行测试,示例如下:

$> cd $GOPATH/src/github.com/vladimirvivien/learning-go/ch12/
$> go test ./vector
3. 过滤执行的测试

在开发大量测试函数时,在调试阶段通常需要专注于某个(或一组)函数。Go 测试命令行工具支持 -run 标志,该标志指定一个正则表达式,仅执行名称与指定表达式匹配的函数。例如,以下命令将仅执行测试函数 TestVectorAdd

$> go test -run=VectorAdd -v

使用 -v 标志可以确认仅执行了一个测试函数。另外,以下命令将执行所有以 VectorA.*$ 结尾或匹配函数名 TestVectorMag 的测试函数,同时忽略其他函数:

> go test -run="VectorA.*$|TestVectorMag" -v
4. 测试日志

在编写新的测试函数或调试现有测试函数时,将信息打印到标准输出通常很有帮助。 testing.T 类型提供了两种日志记录方法: Log (使用默认格式化器)和 Logf (使用格式化动词格式化输出)。例如,以下是 vector 示例中的测试函数片段,展示了如何使用 t.Logf 记录信息:

func TestVectorUnit(t *testing.T) {
   v := New(5.581, -2.136)
   mag := v.Mag()
   expect := New((1/mag)*v[0], (1/mag)*v[1])
   if !v.Unit().Eq(expect) {
       t.Logf("Vector Unit failed, expecting %s, got %s",
           expect, v.Unit())
       t.Fail()
   }
   t.Logf("Vector = %v; Unit vector = %v\n", v, expect)
}

Go 测试工具在没有测试失败时,运行测试的输出非常少。但是,当提供 -v 详细标志时,工具将输出测试日志。例如:

$> go test -run=VectorUnit -v
5. 报告失败

默认情况下,如果测试函数正常运行且没有发生 panic ,Go 测试运行时会认为测试成功。例如,以下测试函数存在问题,因为其预期值计算不正确,但测试运行时会始终报告为通过,因为它没有包含报告失败的代码:

func TestVectorDotProd(t *testing.T) {
    v1 := New(7.887, 4.138).(SimpleVector)
    v2 := New(-8.802, 6.776).(SimpleVector)
    actual := v1.DotProd(v2)
    expect := v1[0]*v2[0] - v1[1]*v2[1]
    if actual != expect {
        t.Logf("DotPoduct failed, expecting %d, got %d",
          expect, actual)
    }
}

可以通过使用 testing.T 类型的 Fail 方法来修复这个问题,示例代码如下:

func TestVectorDotProd(t *testing.T) {
    // ...
    if actual != expect {
        t.Logf("DotPoduct failed, expecting %d, got %d",
          expect, actual)
        t.Fail()
    }
}

需要注意的是, Fail 方法仅报告失败,不会停止测试函数的执行。而当在测试条件失败时需要立即退出函数时,测试 API 提供了 FailNow 方法,它会标记失败并退出当前正在执行的测试函数。

6. 跳过测试

由于环境限制、资源可用性或环境设置不当等因素,有时需要跳过某些测试函数。测试 API 允许使用 testing.T 类型的 SkipNow 方法跳过测试函数。以下代码片段仅在设置了名为 RUN_ANGLE 的任意操作系统环境变量时才会运行测试函数,否则将跳过该测试:

func TestVectorAngle(t *testing.T) {
   if os.Getenv("RUN_ANGLE") == "" {
         t.Skipf("Env variable RUN_ANGLE not set, skipping:")
   }
   v1 := New(3.183, -7.627)
   v2 := New(-2.668, 5.319)
   actual := v1.Angle(v2)
   expect := math.Acos(v1.DotProd(v2) / (v1.Mag() * v2.Mag()))
   if actual != expect {
         t.Logf("Vector angle failed, expecting %d, got %d",
            expect, actual)
         t.Fail()
   }
   t.Log("Angle between", v1, "and", v2, "=", actual)
}

代码中使用了 Skipf 方法,它是 SkipNow Logf 方法的组合。当在未设置环境变量的情况下执行测试时,输出如下:

$> go test -run=Angle -v

当提供环境变量时,测试将按预期执行:

> RUN_ANGLE=1 go test -run=Angle -v
7. 表驱动测试

在 Go 语言中,经常会遇到使用表驱动测试的技术。这种方法将一组输入和预期输出存储在一个数据结构中,然后通过循环遍历不同的测试场景。例如,以下测试函数使用 cases 变量(类型为 []struct{vec SimpleVector; expected float64} )存储多个向量值及其预期的模值,用于测试向量的 Mag 方法:

func TestVectorMag(t *testing.T) {
   cases := []struct{
         vec SimpleVector
         expected float64
   }{
       {New(1.2, 3.4), math.Sqrt(1.2*1.2 + 3.4*3.4)},
       {New(-0.21, 7.47), math.Sqrt(-0.21*-0.21 + 7.47*7.47)},
       {New(1.43, -5.40), math.Sqrt(1.43*1.43 + -5.40*-5.40)},
       {New(-2.07, -9.0), math.Sqrt(-2.07*-2.07 + -9.0*-9.0)},
   }
   for _, c := range cases {
       mag := c.vec.Mag()
       if mag != c.expected {
         t.Errorf("Magnitude failed, execpted %d, got %d",
              c.expected, mag)
       }
   }
}

在每次循环迭代中,代码将 Mag 方法计算的值与预期值进行比较。通过这种方法,可以测试多种输入组合及其相应的输出。该技术可以根据需要扩展,例如添加 name 字段为每个测试用例命名,或者在测试用例结构体中包含一个函数字段,为每个用例指定自定义逻辑。

8. HTTP 测试

Go 语言提供了一流的 API 来构建基于 HTTP 的客户端和服务器程序。 net/http/httptest 子包(Go 标准库的一部分)有助于实现 HTTP 服务器和客户端代码的自动化测试。

8.1 实现简单的 API 服务

为了探索这一领域,我们将实现一个简单的 API 服务,将前面章节中涉及的向量操作作为 HTTP 端点暴露出来。部分服务器代码示例如下:

package main
import (
   "encoding/json"
   "fmt"
   "net/http"
   "github.com/vladimirvivien/learning-go/ch12/vector"
)
func add(resp http.ResponseWriter, req *http.Request) {
   var params []vector.SimpleVector
   if err := json.NewDecoder(req.Body).Decode(&params);
       err != nil {
         resp.WriteHeader(http.StatusBadRequest)
         fmt.Fprintf(resp, "Unable to parse request: %s\n", err)
         return
   }
   if len(params) != 2 {
         resp.WriteHeader(http.StatusBadRequest)
         fmt.Fprintf(resp, "Expected 2 or more vectors")
         return
   }
   result := params[0].Add(params[1])
   if err := json.NewEncoder(resp).Encode(&result); err != nil {
         resp.WriteHeader(http.StatusInternalServerError)
         fmt.Fprintf(resp, err.Error())
         return
   }
}
// ...
func main() {
   mux := http.NewServeMux()
   mux.HandleFunc("/vec/add", add)
   mux.HandleFunc("/vec/sub", sub)
   mux.HandleFunc("/vec/dotprod", dotProd)
   mux.HandleFunc("/vec/mag", mag)
   mux.HandleFunc("/vec/unit", unit)
   if err := http.ListenAndServe(":4040", mux); err != nil {
         fmt.Println(err)
   }
}

每个函数(如 add sub dotprod mag unit )都实现了 http.Handler 接口,用于处理客户端的 HTTP 请求,计算向量包中的相应操作。为了简单起见,请求和响应都使用 JSON 格式。

8.2 测试 HTTP 服务器代码

在编写 HTTP 服务器代码时,需要以健壮且可重复的方式对代码进行测试,而无需设置一些脆弱的代码框架来模拟端到端测试。 httptest.ResponseRecorder 类型专门用于通过检查被测试函数中 http.ResponseWriter 的状态变化,为测试 HTTP 处理程序方法提供单元测试功能。以下代码片段使用 httptest.ResponseRecorder 来测试服务器的 add 方法:

import (
   "net/http"
   "net/http/httptest"
   "strconv"
   "strings"
   "testing"
   "github.com/vladimirvivien/learning-go/ch12/vector"
)
func TestVectorAdd(t *testing.T) {
   reqBody := "[[1,2],[3,4]]"
   req, err := http.NewRequest(
        "POST", "http://0.0.0.0/", strings.NewReader(reqBody))
   if err != nil {
         t.Fatal(err)
   }
   actual := vector.New(1, 2).Add(vector.New(3, 4))
   w := httptest.NewRecorder()
   add(w, req)
   if actual.String() != strings.TrimSpace(w.Body.String()) {
         t.Fatalf("Expecting actual %s, got %s",
             actual.String(), w.Body.String(),
         )
   }
}

代码中使用 http.NewRequest 创建一个新的 *http.Request 值,包含 POST 方法、一个虚假的 URL 和编码为 JSON 数组的请求体。然后使用 httptest.NewRecorder 创建一个 httputil.ResponseRecorder 值,并调用 add(w, req) 函数。最后,将 add 函数执行过程中记录在 w 中的值与预期值进行比较。

8.3 测试 HTTP 客户端代码

为 HTTP 客户端创建测试代码更为复杂,因为需要一个运行的服务器来进行适当的测试。幸运的是, httptest 包提供了 httptest.Server 类型,可以通过编程方式创建服务器,用于测试客户端请求并向客户端返回模拟响应。

以下是部分 HTTP 客户端代码示例:

type vecClient struct {
    svcAddr string
    client *http.Client
}
func (c *vecClient) add(
   vec0, vec1 vector.SimpleVector) (vector.SimpleVector, error) {
   uri := c.svcAddr + "/vec/add"
   // encode params
   var body bytes.Buffer
    params := []vector.SimpleVector{vec0, vec1}
   if err := json.NewEncoder(&body).Encode(&params); err != nil {
         return []float64{}, err
   }
   req, err := http.NewRequest("POST", uri, &body)
   if err != nil {
        return []float64{}, err
   }
   // send request
   resp, err := c.client.Do(req)
   if err != nil {
       return []float64{}, err
   }
   defer resp.Body.Close()
   // handle response
   var result vector.SimpleVector
   if err := json.NewDecoder(resp.Body).
        Decode(&result); err != nil {
        return []float64{}, err
    }
    return result, nil
}

我们可以使用 httptest.Server 类型创建代码来测试客户端发送的请求,并将数据返回给客户端代码进行进一步检查。 httptest.NewServer 函数接受一个 http.Handler 类型的值,其中封装了服务器的测试逻辑,并返回一个新的运行中的 HTTP 服务器,该服务器将在系统选择的端口上提供服务。以下测试函数展示了如何使用 httptest.Server 来测试前面客户端代码中的 add 方法:

import(
    "net/http"
    "net/http/httptest"
    // ...
)
func TestClientAdd(t *testing.T) {
   server := httptest.NewServer(http.HandlerFunc(
         func(resp http.ResponseWriter, req *http.Request) {
             // test incoming request path
             if req.URL.Path != "/vec/add" {
                 t.Errorf("unexpected request path %s",
                    req.URL.Path)
                   return
               }
               // test incoming params
               body, _ := ioutil.ReadAll(req.Body)
               params := strings.TrimSpace(string(body))
               if params != "[[1,2],[3,4]]" {
                     t.Errorf("unexpected params '%v'", params)
                     return
               }
               // send result
               result := vector.New(1, 2).Add(vector.New(3, 4))
               err := json.NewEncoder(resp).Encode(&result)
               if err != nil {
                     t.Fatal(err)
                     return
               }
         },
   ))
   defer server.Close()
   client := newVecClient(server.URL)
   expected := vector.New(1, 2).Add(vector.New(3, 4))
   result, err := client.add(vector.New(1, 2), vector.New(3, 4))
   if err != nil {
         t.Fatal(err)
   }
   if !result.Eq(expected) {
         t.Errorf("Expecting %s, got %s", expected, result)
   }
}

测试函数首先设置服务器及其处理函数。在 http.HandlerFunc 函数内部,代码首先确保客户端请求的路径为 /vec/add ,然后检查客户端的请求体,确保其为正确的 JSON 格式且包含有效的 add 操作参数。最后,处理函数将预期结果编码为 JSON 并作为响应发送给客户端。使用系统生成的服务器地址创建一个新的客户端,并调用 client.add 方法发送请求。将返回的结果与预期值进行比较,以确保 add 方法正常工作。

9. 测试覆盖率

在编写测试时,了解实际代码中有多少被测试覆盖是很重要的。这个覆盖率数字可以反映测试逻辑对源代码的覆盖程度。在许多软件开发实践中,测试覆盖率是一个关键指标,因为它衡量了代码的测试质量。

Go 测试工具自带了一个内置的覆盖率工具。使用 -cover 标志运行 go test 命令会在原始源代码中插入覆盖率逻辑,然后运行生成的测试二进制文件,并提供包的整体覆盖率摘要,示例如下:

$> go test -cover

结果显示代码的测试覆盖率为 87.8%。我们可以使用测试工具提取更多关于被测试代码部分的详细信息。通过使用 -coverprofile 标志将覆盖率指标记录到一个文件中,示例如下:

$> go test -coverprofile=cover.out
9.1 使用 cover 工具

一旦保存了覆盖率数据,可以使用 go tool cover 命令以文本表格格式呈现这些数据。以下是前面生成的覆盖率文件中每个测试函数覆盖率指标的部分输出示例:

$> go tool cover -func=cover.out

cover 工具还可以将覆盖率指标叠加到实际代码上,直观地显示代码中被覆盖(绿色)和未被覆盖(红色)的部分。使用 -html 标志可以根据之前收集的覆盖率数据生成一个 HTML 页面:

$> go tool cover -html=cover.out

该命令会打开默认的 Web 浏览器并显示覆盖率数据。

总结

本文详细介绍了 Go 语言中的代码测试相关内容,包括测试函数的基本原理、测试的运行和过滤、日志记录、失败报告、跳过测试、表驱动测试、HTTP 测试以及测试覆盖率等方面。通过这些技术和方法,可以有效地对 Go 代码进行全面、高效的测试,提高代码的质量和可靠性。

以下是一个简单的流程图,展示了测试函数的基本流程:

graph TD;
    A[设置预期值] --> B[运行被测试代码获取实际值];
    B --> C[比较实际值和预期值];
    C -->|相等| D[测试通过];
    C -->|不相等| E[记录错误信息];
    E --> F[标记测试失败];

同时,为了更清晰地展示不同测试类型的特点,我们列出以下表格:
| 测试类型 | 特点 | 适用场景 |
| ---- | ---- | ---- |
| 基础测试 | 设定预期值并与实际值比较 | 简单功能测试 |
| 表驱动测试 | 使用数据结构存储多组输入和预期输出 | 多种输入组合测试 |
| HTTP 服务器测试 | 模拟请求和响应进行测试 | 验证服务器接口功能 |
| HTTP 客户端测试 | 需要运行服务器配合测试 | 确保客户端请求和响应处理正常 |
| 测试覆盖率分析 | 了解代码被测试的覆盖程度 | 评估测试完整性 |

Go语言代码测试全解析

10. 测试工具使用总结

在Go语言的代码测试中,我们使用了多个工具和命令,下面对这些工具和命令进行总结:
| 工具/命令 | 作用 | 示例 |
| ---- | ---- | ---- |
| go test | 执行测试函数 | go test . 运行当前包的所有测试函数 |
| go test -run | 过滤执行的测试函数,通过正则表达式匹配函数名 | go test -run=VectorAdd -v 只执行 TestVectorAdd 函数 |
| go test -v | 显示详细的测试日志 | go test -run=VectorUnit -v |
| go test -cover | 显示测试覆盖率 | go test -cover |
| go test -coverprofile | 将覆盖率指标记录到文件 | go test -coverprofile=cover.out |
| go tool cover -func | 以文本表格格式呈现覆盖率数据 | go tool cover -func=cover.out |
| go tool cover -html | 生成 HTML 页面显示覆盖率数据 | go tool cover -html=cover.out |

11. 不同测试方法的应用场景分析

不同的测试方法适用于不同的场景,下面我们来分析一下:
- 基础测试 :适用于简单功能的测试,例如单个函数的功能验证。通过设定预期值并与实际值比较,能够快速判断函数是否按预期工作。如前面提到的向量加法测试,只需要简单地计算预期结果并与实际结果对比。
- 表驱动测试 :当需要测试多种输入组合及其相应输出时,表驱动测试非常有用。它可以将多组输入和预期输出存储在一个数据结构中,通过循环遍历进行测试。例如测试向量的 Mag 方法,使用表驱动测试可以方便地测试多个不同的向量。
- HTTP 服务器测试 :用于验证服务器接口的功能。通过模拟请求和响应,能够检查服务器是否正确处理请求并返回预期的结果。如测试向量操作的 HTTP 服务器,使用 httptest.ResponseRecorder 可以模拟请求并检查响应。
- HTTP 客户端测试 :确保客户端请求和响应处理正常。由于需要一个运行的服务器配合测试,使用 httptest.Server 可以方便地创建一个测试服务器,模拟服务器的行为并返回数据给客户端进行检查。
- 测试覆盖率分析 :用于评估测试的完整性。通过分析测试覆盖率,可以了解代码中有多少部分被测试覆盖,从而发现可能未被测试到的代码区域。

12. 测试代码的优化建议

为了提高测试代码的质量和可维护性,我们可以采取以下优化建议:
- 代码复用 :在测试代码中,可能会有一些重复的操作,例如创建测试数据、设置请求等。可以将这些重复的操作封装成函数,提高代码的复用性。例如,在多个 HTTP 测试中都需要创建请求,可以将创建请求的代码封装成一个函数。
- 错误处理 :在测试代码中,要确保对可能出现的错误进行适当的处理。例如,在创建请求、解析 JSON 等操作时,可能会出现错误,应该使用 t.Fatal t.Error 等方法及时报告错误。
- 注释和文档 :为测试代码添加详细的注释和文档,解释每个测试的目的和预期结果。这样可以提高代码的可读性,方便其他开发者理解和维护测试代码。
- 并行测试 :对于一些独立的测试用例,可以考虑使用并行测试来提高测试效率。Go 语言的测试框架支持并行测试,可以使用 t.Parallel() 方法将测试用例标记为并行执行。

13. 未来测试技术的展望

随着软件开发的不断发展,测试技术也在不断进步。在 Go 语言的测试领域,未来可能会出现以下趋势:
- 自动化测试框架的发展 :会有更多功能强大、易用的自动化测试框架出现,帮助开发者更高效地编写和执行测试代码。
- 与持续集成/持续部署(CI/CD)的深度融合 :测试将更加紧密地集成到 CI/CD 流程中,实现代码提交后自动进行测试,确保代码质量。
- 人工智能在测试中的应用 :利用人工智能技术进行测试用例的生成、缺陷预测等,提高测试的效率和准确性。

以下是一个流程图,展示了从编写测试代码到优化测试代码的过程:

graph TD;
    A[编写测试代码] --> B[运行测试];
    B -->|有错误| C[调试测试代码];
    C --> B;
    B -->|无错误| D[分析测试覆盖率];
    D -->|覆盖率低| E[补充测试用例];
    E --> B;
    D -->|覆盖率高| F[优化测试代码];
    F --> G[提高代码复用性];
    F --> H[完善错误处理];
    F --> I[添加注释和文档];
    F --> J[考虑并行测试];

通过以上对 Go 语言代码测试的全面介绍,我们了解了各种测试方法、工具的使用以及测试代码的优化和未来发展趋势。希望这些内容能够帮助开发者更好地进行 Go 代码的测试,提高代码的质量和可靠性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值