跳转到主要内容

🚀 我的新课程 - Golang 测试圣经现已推出,涵盖了启动和运行为 Go 应用程序创建测试所需的一切!

欢迎各位码农!在本教程中,我们将看看 Go 核心语言开发人员和流行的生产级工具等使用的更高级的测试实践的选择。

我觉得这种方法,即实际研究在生产系统中所做的事情,有望让您深入了解测试您自己的生产级 Go 程序的最佳方法。

注意 - 如果您对测试基于 Go 的程序完全不熟悉,那么我建议您查看我的另一个教程:go 测试简介

目标


在本教程结束时:

  • 您将对如何测试 Go 系统有更深入的了解
  • 您将知道如何分离您的单元和集成测试。
  • 您将知道如何模拟您的 Go 应用程序可能发出的任何 HTTP 请求。


先决条件


在您阅读本文之前,您将需要以下内容:

  • 您需要在开发机器上安装 Go 版本 1.11+。

通过表驱动测试实现良好的覆盖率


让我们在字符串包中开始我们的旅程。如果您查看 src/strings/ 中 strings_test.go 文件的顶部,您应该会看到许多定义和填充的数组。

例如,看看 lastIndexTests,它是一个 IndexTest 类型的数组:

var lastIndexTests = []IndexTest{
    {"", "", 0},
    {"", "a", -1},
    {"", "foo", -1},
    {"fo", "foo", -1},
    {"foo", "foo", 0},
    {"foo", "f", 0},
    {"oofofoofooo", "f", 7},
    {"oofofoofooo", "foo", 7},
    {"barfoobarfoo", "foo", 9},
    {"foo", "", 3},
    {"foo", "o", 2},
    {"abcABCabc", "A", 3},
    {"abcABCabc", "a", 6},
}


该数组用于测试 strings.go 文件中的 LastIndex 函数,其中包含多个否定和肯定案例。这些 IndexTest 元素中的每一个都具有标准字符串、分隔符和输出整数值,并具有如下所示的结构:

type IndexTest struct {
    s   string
    sep string
    out int
}


然后这些测试由 TestLastIndex() 函数触发,该函数遍历所有这些测试用例并检查从 lastIndex 函数返回的结果是否与数组中列出的预期结果相匹配。

对于许多不同的功能,同样的做法会重复多次,这有助于保证当对这些功能进行任何代码更改时,预期的功能不会改变。

使用 testdata 目录


在某些情况下,您将无法将预期的输入和输出指定为上例中的元素数组。您可能正在尝试测试如何读取和写入文件系统上的文件或如何解析专有数据格式等。

如果是这种情况,那么一种选择是创建一个 testdata 目录并将测试所需的任何文件存储在该目录中。

在 src/archive/tar/ 下的标准库中可以再次找到一个很好的示例,其中定义了 testdata/ 目录并包含许多随后用于测试的 .tar 文件。

在 reader_test.go 文件中可以找到使用这些文件的一些相当复杂的测试示例。

func TestReader(t *testing.T) {
    vectors := []struct {
        file    string    // Test input file
        headers []*Header // Expected output headers
        chksums []string  // MD5 checksum of files, leave as nil if not checked
        err     error     // Expected error to occur
    }{{
        file: "testdata/gnu.tar",
        headers: []*Header{{
            Name:     "small.txt",
            Mode:     0640,
            Uid:      73025,
            Gid:      5000,
            Size:     5,
            ModTime:  time.Unix(1244428340, 0),
            Typeflag: '0',
            Uname:    "dsymonds",
            Gname:    "eng",
            Format:   FormatGNU,
        }, {
            Name:     "small2.txt",
            Mode:     0640,
            Uid:      73025,
            Gid:      5000,
            Size:     11,
            ModTime:  time.Unix(1244436044, 0),
            Typeflag: '0',
            Uname:    "dsymonds",
            Gname:    "eng",
            Format:   FormatGNU,
        }},
        chksums: []string{
            "e38b27eaccb4391bdec553a7f3ae6b2f",
            "c65bd2e50a56a2138bf1716f2fd56fe9",
        },
  },
  // more test cases

在上面的函数中,您会看到核心开发人员将我们在此介绍的第一种技术与 testdata/ 目录中的文件相结合,以确保在打开示例 .tar 文件时,文件及其校验和符合他们的预期。

模拟 HTTP 请求


一旦您开始编写生产级 API 和服务,您很可能会开始与其他服务交互,并且能够测试与这些服务交互的方式与测试代码库的其他部分一样重要。

但是,您可能正在与对数据库执行 CRUD 操作的 REST API 进行交互,因此,当您只是试图测试事情是否正常时,您不希望这些更改实际上被提交到您的数据库。

因此,为了解决这个问题,我们可以使用 net/http/httptest 包来模拟 HTTP 响应,这是我们在这些情况下最好的朋友。

main_test.go

package main_test

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHttp(t *testing.T) {
  //
    handler := func(w http.ResponseWriter, r *http.Request) {
    // here we write our expected response, in this case, we return a
    // JSON string which is typical when dealing with REST APIs
        io.WriteString(w, "{ \"status\": \"expected service response\"}")
    }

    req := httptest.NewRequest("GET", "https://tutorialedge.net", nil)
    w := httptest.NewRecorder()
    handler(w, req)

    resp := w.Result()
    body, _ := ioutil.ReadAll(resp.Body)
    fmt.Println(resp.StatusCode)
    fmt.Println(resp.Header.Get("Content-Type"))
    fmt.Println(string(body))
}

在上面的测试用例中,我们基本上覆盖了我们期望从我们的 URL 得到的响应,然后继续测试我们系统中依赖该响应的其他部分。

使用单独的包


如果我们查看 strings_test.go 文件并检查顶部的包,您应该注意到它与 strings.go 文件所在的包不同。

这是什么原因?它可以帮助您避免循环导入。在某些情况下,您需要在 *_test.go 文件中导入一个包来充分编写您的测试。如果您导入的包已经引用了您尝试测试的包,您可能会看到循环依赖的问题。

区分您的单元和集成测试
注意 - 我最初是从:Go Advanced Tips Tricks 中发现这个技巧的

如果您正在为大型企业 Go 系统编写测试,那么您很可能会同时拥有一组集成和单元测试来确保系统的有效性。

但是,通常情况下,您会发现与单元测试相比,集成测试需要更长的时间才能运行,因为它们可能会涉及到其他系统。

在这种情况下,将集成测试放入 *_integration_test.go 文件并将 // +build 集成添加到测试文件的顶部是有意义的:

// +build integration

package main_test

import (
    "fmt"
    "testing"
)

func TestMainIntegration(t *testing.T) {
    fmt.Println("My Integration Test")
}

为了运行这套集成测试,你可以像这样调用 go test:

➜  advanced-go-testing-tutorial git:(master) ✗ go test -tags=integration
My Integration Test
PASS
ok      _/Users/elliot/Documents/Projects/tutorials/golang/advanced-go-testing-tutorial 0.006s


结论


因此,在本教程中,我们了解了 Go 语言维护人员使用的一些更高级的测试技术和技巧。

希望您发现这很有用,并且它为您提供了开始和改进自己的 Go 测试所需的洞察力。如果您觉得它有用,或者有任何其他问题,请不要犹豫,在下面的评论部分告诉我!

文章链接