Go语言:编写一个 WebsiteRacer 的函数,用来对比请求两个 URL 来「比赛」,并返回先响应的 URL。如果两个 URL 在 10 秒内都未返回结果,返回一个 error。
问题:
你被要求编写一个叫做 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
本文来自博客园,作者:slowlydance2me,转载请注明原文链接:https://www.cnblogs.com/slowlydance2me/p/17268451.html