首頁  >  文章  >  後端開發  >  在 Go 中輕鬆進行 HTTP 用戶端測試

在 Go 中輕鬆進行 HTTP 用戶端測試

王林
王林原創
2024-07-17 12:24:281010瀏覽

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