首页  >  文章  >  后端开发  >  在 Go 中轻松进行 HTTP 客户端测试

在 Go 中轻松进行 HTTP 客户端测试

王林
王林原创
2024-07-17 12:24:281011浏览

Effortless HTTP Client Testing in Go

介绍

作为一名软件工程师,您可能熟悉编写与外部 HTTP 服务交互的代码。毕竟,这是我们最常见的事情之一!无论是获取数据、处理提供商的付款,还是自动化社交媒体帖子,我们的应用程序几乎总是涉及外部 HTTP 请求。为了使我们的软件可靠且可维护,我们需要一种方法来测试负责执行这些请求并处理可​​能发生的错误的代码。这给我们留下了几个选择:

  • 实现一个可以被主应用程序代码模拟的客户端包装器,这在测试中仍然存在差距
  • 测试响应解析和处理与实际请求执行分开。虽然单独测试这个较低级别的单元可能是个好主意,但如果可以轻松地与实际请求一起覆盖它那就太好了
  • 将测试转移到集成测试,这会减慢开发速度,并且无法测试某些错误场景,并且可能会受到其他服务可靠性的影响

这些选项并不可怕,特别是如果它们可以一起使用,但我们有一个更好的选择:VCR 测试。

VCR 测试,以录像机命名,是一种模拟测试,可根据实际请求生成测试装置。夹具记录请求和响应,以便在将来的测试中自动重用。尽管您之后可能需要修改装置来处理基于时间的动态输入或删除凭据,但这比从头开始创建模拟要简单得多。 VCR 测试还有一些额外的好处:

  • 将代码一直执行到 HTTP 级别,以便您可以端到端地测试您的应用程序
  • 您可以采取真实世界的响应并修改生成的装置以增加响应时间、导致速率限制等,以测试不经常发生的错误场景
  • 如果您的代码使用外部包/库与 API 交互,您可能不确切知道请求和响应是什么样的,因此 VCR 测试可以自动弄清楚
  • 生成的装置还可以用于调试测试并确保您的代码执行预期的请求

使用 Go 进行更深入的研究

现在您已经了解了 VCR 测试背后的动机,让我们更深入地了解如何使用 dnaeon/go-vcr 在 Go 中实现它。

该库无缝集成到任何 HTTP 客户端代码中。如果您的客户端库代码尚未允许设置 *http.Client 或客户端的 http.Transport,您应该立即添加。

对于那些不熟悉的人来说,http.Transport 是 http.RoundTripper 的实现,它基本上是一个可以访问请求/响应的客户端中间件。它对于实现 500 级或 429(速率限制)响应的自动重试,或者添加指标并记录请求非常有用。在这种情况下,它允许 go-vcr 将请求重新路由到其自己的进程内 HTTP 服务器。

URL 缩短器示例

让我们从一个简单的例子开始。我们想要创建一个向免费的 https://cleanuri.com API 发出请求的包。这个包将提供一个函数:Shorten(string) (string, error)

既然这是一个免费的API,也许我们可以通过直接向服务器发出请求来测试它?这可能有效,但可能会导致一些问题:

  • 服务器的速率限制为 2 个请求/秒,如果我们有大量测试,这可能会成为问题
  • 如果服务器出现故障或需要一段时间才能响应,我们的测试可能会失败
  • 虽然缩短的 URL 被缓存,但我们不能保证每次都会得到相同的输出
  • 向免费 API 发送不必要的流量是不礼貌的!

好吧,如果我们创建一个接口并模拟它怎么办?我们的包非常简单,所以这会使它变得过于复杂。由于我们使用的最低级别是 *http.Client,因此我们必须围绕它定义一个新接口并实现一个模拟。

另一个选项是覆盖目标 URL 以使用 httptest.Server 提供的本地端口。这基本上是 go-vcr 功能的简化版本,在我们的简单情况下就足够了,但在更复杂的场景中将无法维护。即使在这个示例中,您也会看到管理生成的装置比管理不同的模拟服务器实现更容易。

由于我们的界面已经定义,并且我们通过在 https://cleanuri.com 尝试 UI 来了解一些有效的输入/输出,因此这是练习测试驱动开发的绝佳机会。我们将首先为我们的 Shorten 函数实现一个简单的测试:

package shortener_test

func TestShorten(t *testing.T) {
    shortened, err := shortener.Shorten("https://dev.to/calvinmclean")
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }

    if shortened != "https://cleanuri.com/7nPmQk" {
        t.Errorf("unexpected result: %v", shortened)
    }
}

非常简单!我们知道测试将无法编译,因为 Shorter.Shorten 未定义,但我们还是运行它,所以修复它会更令人满意。

最后,让我们来实现这个功能:

package shortener

var DefaultClient = http.DefaultClient

const address = "https://cleanuri.com/api/v1/shorten"

// Shorten will returned the shortened URL
func Shorten(targetURL string) (string, error) {
    resp, err := DefaultClient.PostForm(
        address,
        url.Values{"url": []string{targetURL}},
    )
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return "", fmt.Errorf("unexpected response code: %d", resp.StatusCode)
    }

    var respData struct {
        ResultURL string `json:"result_url"`
    }
    err = json.NewDecoder(resp.Body).Decode(&respData)
    if err != nil {
        return "", err
    }

    return respData.ResultURL, nil
}

现在我们的测试通过了!正如我所承诺的那样令人满意。

为了开始使用VCR,我们需要在测试开始时初始化Recorder并覆盖shorter.DefaultClient:

func TestShorten(t *testing.T) {
    r, err := recorder.New("fixtures/dev.to")
    if err != nil {
        t.Fatal(err)
    }
    defer func() {
        require.NoError(t, r.Stop())
    }()

    if r.Mode() != recorder.ModeRecordOnce {
        t.Fatal("Recorder should be in ModeRecordOnce")
    }

    shortener.DefaultClient = r.GetDefaultClient()

    // ...

运行测试以生成fixtures/dev.to.yaml,其中包含有关测试请求和响应的详细信息。当我们重新运行测试时,它使用记录的响应而不是联系服务器。不要只相信我的话;关闭计算机的 WiFi 并重新运行测试!

您可能还会注意到,由于 go-vcr 记录并重放响应持续时间,因此运行测试所需的时间相对一致。您可以在 YAML 中手动修改此字段以加快测试速度。

模拟错误

为了进一步展示这种测试的好处,我们添加另一个功能:由于速率限制而在 429 响应后重试。由于我们知道 API 的速率限制是每秒,因此 Shorten 可以在收到 429 响应代码时自动等待一秒钟并重试。

我尝试直接使用 API 重现此错误,但似乎在考虑速率限制之前它会使用缓存中的现有 URL 进行响应。这次我们可以创建自己的模拟,而不是用虚假 URL 污染缓存。

这是一个简单的过程,因为我们已经生成了灯具。将fixtures/dev.to.yaml复制/粘贴到新文件后,复制成功的请求/响应交互并将第一个响应的代码从200更改为429。此fixture模拟速率限制失败后的成功重试。

此测试与原始测试之间的唯一区别是新的夹具文件名。预期输出是相同的,因为 Shorten 应该处理错误。这意味着我们可以将测试放入循环中以使其更加动态:

func TestShorten(t *testing.T) {
    fixtures := []string{
        "fixtures/dev.to",
        "fixtures/rate_limit",
    }

    for _, fixture := range fixtures {
        t.Run(fixture, func(t *testing.T) {
            r, err := recorder.New(fixture)
            if err != nil {
                t.Fatal(err)
            }
            defer func() {
                require.NoError(t, r.Stop())
            }()

            if r.Mode() != recorder.ModeRecordOnce {
                t.Fatal("Recorder should be in ModeRecordOnce")
            }

            shortener.DefaultClient = r.GetDefaultClient()

            shortened, err := shortener.Shorten("https://dev.to/calvinmclean")
            if err != nil {
                t.Errorf("unexpected error: %v", err)
            }

            if shortened != "https://cleanuri.com/7nPmQk" {
                t.Errorf("unexpected result: %v", shortened)
            }
        })
    }
}

新的测试再次失败。这次由于429响应未处理,所以让我们实现新功能以通过测试。为了保持简单性,我们的函数使用 time.Sleep 和递归调用来处理错误,而不是处理考虑最大重试和指数退避的复杂性:

func Shorten(targetURL string) (string, error) {
    // ...
    switch resp.StatusCode {
    case http.StatusOK:
    case http.StatusTooManyRequests:
        time.Sleep(time.Second)
        return Shorten(targetURL)
    default:
        return "", fmt.Errorf("unexpected response code: %d", resp.StatusCode)
    }
    // ...

现在再次运行测试并查看它们是否通过!

自己更进一步,尝试添加错误请求的测试,当使用像 my-fake-url 这样的无效 URL 时,就会发生这种情况。

此示例的完整代码(以及错误请求测试)可在 Github 上找到。

结论

VCR 测试的好处从这个简单的示例中就可以清楚地看出,但在处理请求和响应难以处理的复杂应用程序时,它们的影响力更大。我鼓励您在自己的应用程序中尝试一下,而不是处理乏味的模拟或选择根本不进行测试。如果您已经依赖集成测试,那么开始使用 VCR 会更容易,因为您已经有了可以生成固定装置的真实请求。

在包的 Github 存储库中查看更多文档和示例:https://github.com/dnaeon/go-vcr

以上是在 Go 中轻松进行 HTTP 客户端测试的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn