問題:
你被要求編寫一個叫做 WebsiteRacer
的函式,用來對比請求兩個 URL 來「比賽」,並返回先響應的 URL。如果兩個 URL 在 10 秒內都未返回結果,那麼應該返回一個 error
。
-
net/http
用來呼叫 HTTP 請求
-
net/http/httptest
用來測試這些請求
-
Go 程(goroutines)
-
select
先寫測試
func TestRacer(t *testing.T) { slowURL := "http://www.facebook.com" fastURL := "http://www.quii.co.uk" want := fastURL got := Racer(slowURL, fastURL) if got != want { t.Errorf("got '%s', want '%s'", got, want) } }
我們知道這樣不完美並且有問題,但這樣可以把事情開展起來。重要的是,不要徘徊在第一次就想把事情做到完美。
嘗試執行測試
./racer_test.go:14:9: undefined: Racer
為測試的執行編寫最少量的程式碼,並檢查失敗測試的輸出
func Racer(a, b string) (winner string) { return }
racer_test.go:25: got '', want 'http://www.quii.co.uk'
編寫足夠的程式碼使程式透過
func Racer(a, b string) (winner string) { startA := time.Now() http.Get(a) aDuration := time.Since(startA) startB := time.Now() http.Get(b) bDuration := time.Since(startB) if aDuration < bDuration { return a } return b }
-
1.我們用
time.Now()
來記錄請求URL
前的時間。
-
2.然後用
http.Get
來請求URL
的內容。這個函式返回一個http.Response
和一個error
,但目前我們不關心它們的值。
-
3.
time.Since
獲取開始時間並返回一個time.Duration
時間差。
我們完成這些後就可以透過對比請求耗時來找出最快的了。
問題
-
速度慢
-
不可靠
- 無法進行邊界條件測試
net/http/httptest
包,它可以讓你輕易建立一個 HTTP 模擬伺服器(mock HTTP server)。func TestRacer(t *testing.T) { slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(20 * time.Millisecond) w.WriteHeader(http.StatusOK) })) fastServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) slowURL := slowServer.URL fastURL := fastServer.URL want := fastURL got := Racer(slowURL, fastURL) if got != want { t.Errorf("got '%s', want '%s'", got, want) } slowServer.Close() fastServer.Close() }
語法看著有點兒複雜,沒關係,慢慢來。
httptest.NewServer
接受一個我們傳入的 匿名函式 http.HandlerFunc
。http.HandlerFunc
是一個看起來類似這樣的型別:type HandlerFunc func(ResponseWriter, *Request)
。ResponseWriter
和 Request引數的函式,這對於 HTTP 伺服器來說並不奇怪。
httptest.NewServer
,它會找一個可監聽的埠,然後測試完你就可以關閉它了。time.Sleep
一段時間,當我們請求時讓它比另一個慢一些。然後兩個伺服器都會透過 w.WriteHeader(http.StatusOK)
返回一個 OK
給呼叫者。重構
我們在主程式程式碼和測試程式碼裡都有一些重複。
func Racer(a, b string) (winner string) { aDuration := measureResponseTime(a) bDuration := measureResponseTime(b) if aDuration < bDuration { return a } return b } func measureResponseTime(url string) time.Duration { start := time.Now() http.Get(url) return time.Since(start) }
這樣簡化程式碼後可以讓 Racer
函式更加易讀。
func TestRacer(t *testing.T) { slowServer := makeDelayedServer(20 * time.Millisecond) fastServer := makeDelayedServer(0 * time.Millisecond) defer slowServer.Close() defer fastServer.Close() slowURL := slowServer.URL fastURL := fastServer.URL want := fastURL got := Racer(slowURL, fastURL) if got != want { t.Errorf("got '%s', want '%s'", got, want) } } func makeDelayedServer(delay time.Duration) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(delay) w.WriteHeader(http.StatusOK) })) }
我們透過一個名為 makeDelayedServer
的函式重構了模擬伺服器,以將一些不感興趣的程式碼移出測試並減少了重複程式碼。
defer
defer
字首會在 包含它的函式結束時 呼叫它。程式同步
-
Go 在併發方面很在行,為什麼我們要一個接一個地測試哪個網站更快呢?我們應該能夠同時測試兩個。
- 我們並不關心請求的 準確響應時間,我們只是需要知道哪個更快返回而已。
select
的新構造(construct),它可以幫我們輕易清晰地實現程式同步。
func Racer(a, b string) (winner string) { select { case <-ping(a): return a case <-ping(b): return b } } func ping(url string) chan bool { ch := make(chan bool) go func() { http.Get(url) ch <- true }() return ch }
ping
chan bool
型別並返回它的 ping
函式。http.Get(url)
時啟動了一個用來給 channel 傳送訊號的 Go 程(goroutine)。select
myVar := <-ch
來等待值傳送給 channel。這是一個 阻塞 的呼叫,因為你需要等待值返回。select
則允許你同時在 多個 channel 等待。第一個傳送值的 channel「勝出」,case
中的程式碼會被執行。select
中使用 ping
為兩個 URL
設定兩個 channel。無論哪個先寫入其 channel 都會使 select
裡的程式碼先被執行,這會導致那個 URL
先被返回(勝出)。Racer
耗時超過 10 秒時返回一個 error。先寫測試
t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) {
serverA := makeDelayedServer(11 * time.Second)
serverB := makeDelayedServer(12 * time.Second)
defer serverA.Close()
defer serverB.Close()
_, err := Racer(serverA.URL, serverB.URL)
if err == nil {
t.Error("expected an error but didn't get one")
}
})
為了練習這個場景,現在我們要使模擬伺服器超過 10 秒後返回兩個值,勝出的 URL(這個測試中我們用 _
忽略掉了)和一個 error
。
嘗試執行測試
./racer_test.go:37:10: assignment mismatch: 2 variables but 1 values
為測試的執行編寫最少量的程式碼,並檢查失敗測試的輸出
func Racer(a, b string) (winner string, error error) { select { case <-ping(a): return a, nil case <-ping(b): return b, nil } }
Racer
的函式簽名來返回勝出者和一個 error
。返回 nil
僅用於模擬順利的場景(happy cases)。got, _ := Racer(slowURL, fastURL)
,要知道順利的場景中我們不應得到一個 error
。--- FAIL: TestRacer (12.00s)
--- FAIL: TestRacer/returns_an_error_if_a_server_doesn't_respond_within_10s (12.00s)
racer_test.go:40: expected an error but didn't get one
編寫足夠的程式碼使程式透過
func Racer(a, b string) (winner string, error error) { select { case <-ping(a): return a, nil case <-ping(b): return b, nil case <-time.After(10 * time.Second): return "", fmt.Errorf("timed out waiting for %s and %s", a, b) } }
select
時,time.After
是一個很好用的函式。當你監聽的 channel 永遠不會返回一個值時你可以潛在地編寫永遠阻塞的程式碼,儘管在我們的案例中它沒有發生。time.After
會在你定義的時間過後傳送一個訊號給 channel 並返回一個 chan
型別(就像 ping
那樣)。a
或 b
誰勝出就返回誰,但如果測試達到 10 秒,那麼 time.After
會傳送一個訊號並返回一個 error
慢速測試
func Racer(a, b string, timeout time.Duration) (winner string, error error) { select { case <-ping(a): return a, nil case <-ping(b): return b, nil case <-time.After(timeout): return "", fmt.Errorf("timed out waiting for %s and %s", a, b) } }
- 在順利的情況「happy test」下我們是否關心超時時間?
- 需求對超時時間很明確
鑑於以上資訊,我們再做一次小的重構來讓我們的測試和程式碼的使用者合意
var tenSecondTimeout = 10 * time.Second func Racer(a, b string) (winner string, error error) { return ConfigurableRacer(a, b, tenSecondTimeout) } func ConfigurableRacer(a, b string, timeout time.Duration) (winner string, error error) { select { case <-ping(a): return a, nil case <-ping(b): return b, nil case <-time.After(timeout): return "", fmt.Errorf("timed out waiting for %s and %s", a, b) } }
我們的使用者和第一個測試可以使用 Racer
(使用 ConfigurableRacer
),不順利的場景測試可以使用 ConfigurableRacer
。
func TestRacer(t *testing.T) { t.Run("compares speeds of servers, returning the url of the fastest one", func(t *testing.T) { slowServer := makeDelayedServer(20 * time.Millisecond) fastServer := makeDelayedServer(0 * time.Millisecond) defer slowServer.Close() defer fastServer.Close() slowURL := slowServer.URL fastURL := fastServer.URL want := fastURL got, err := Racer(slowURL, fastURL) if err != nil { t.Fatalf("did not expect an error but got one %v", err) } if got != want { t.Errorf("got '%s', want '%s'", got, want) } }) t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) { server := makeDelayedServer(25 * time.Millisecond) defer server.Close() _, err := ConfigurableRacer(server.URL, server.URL, 20*time.Millisecond) if err == nil { t.Error("expected an error but didn't get one") } }) }
在第一個測試最後加了一個檢查來驗證我們沒得到一個 error
總結
我們學到了什麼?
select
-
可幫助你同時在多個 channel 上等待。
- 有時你想在你的某個「案例」中使用
time.After
httptest
-
一種方便地建立測試伺服器的方法,這樣你就可以進行可靠且可控的測試。
- 使用和
net/http