選択
Select
2つのURLを取得し、それらをHTTP GETでヒットして最初に返されたURLを返すことで「競合」する
WebsiteRacer
と呼ばれる関数を作成するように求められました。 10秒以内に戻らない場合は、「エラー(error
)」を返します。これには
- HTTP呼び出しを行うための
net/http
。 net/http/httptest
はテストを支援するためのものです。- ゴルーチン。
- プロセスを同期するための
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 %q, want %q", 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
}
各URLについて
- 1.
time.Now()
を使用して、URL
を取得しようとする直前に記録します。 - 2.
- 3.
time.Since
は開始時間を取り、差のtime.Duration
を返します。
これを実行したら、期間を比較してどちらが最も速いかを確認します。
これにより、テストに合格する場合と合格しない場合があります。 問題は、実際のWebサイトに連絡して、独自のロジックをテストしていることです。
HTTPを使用するコードのテストは非常に一般的であるため、Goの標準ライブラリには、テストに役立つツールがあります。
モックと依存性注入の章では、コードをテストするために外部サービスに依存したくないという理想的な方法について説明しました。
- スロー(Slow)
- フレーク状(Flaky)
- エッジケースをテストできません(Can't test edge cases)
テストをモックを使用するように変更して、制御できる信頼性の高いサーバーをテストできるようにします。
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 %q, want %q", got, want)
}
slowServer.Close()
fastServer.Close()
}
構文は少しせわしなく見えるかもしれませんが、時間をかけてください。
httptest.NewServer
は、anonymous function を介して送信するhttp.HandlerFunc
を受け取ります。http.HandlerFunc
は、type HandlerFunc func(ResponseWriter, *Request)
のようなタイプです。実際に言っているのは、
ResponseWriter
とRequest
を受け取る関数が必要なことだけです。 これは、HTTPサーバーにとってそれほど驚くべきことではありません。ここには特別な魔法はありません。これは、Goで _ 実際に _ HTTPサーバーを作成する方法でもあります。唯一の違いは、それを
httptest.NewServer
でラップすることです。これにより、リッスンする開いているポートが見つかり、テストが完了したら閉じることができるため、テストでの使用が簡単になります。2つのサーバー内では、遅いサーバーに他のサーバーよりも遅いリクエストを受け取ったときに、遅い方の
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)
}
このドライアップ(DRY-ing up)により、
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 %q, want %q", 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
を付けることで、その関数を含まれている関数の最後に呼び出します。場合によっては、ファイルを閉じるなどのリソースをクリーンアップする必要があります。この場合、サーバーがポートをリッスンし続けないようにサーバーを閉じる必要があります。
これを関数の最後に実行したいが、将来のコードの読む人のために、サーバーを作成した場所の近くに命令を置いておきます。
私たちのリファクタリングは改善であり、これまでに取り上げたGo機能を考えると合理的なソリューションですが、ソリューションをよりシンプルにすることができます。
- Goが同時実行性に優れているのに、なぜWebサイトの速度を次々にテストするのですか?両方を同時にチェックできるはずです。
- リクエストの「正確な応答時間」については特に気にしません。どちらが最初に返されるかを知りたいだけです。
これを行うために、プロセスを 非常に簡単かつ明確に同期するのに役立つ
select
と呼ばれる新しい構成を導入します。func Racer(a, b string) (winner string) {
select {
case <-ping(a):
return a
case <-ping(b):
return b
}
}
func ping(url string) chan struct{} {
ch := make(chan struct{})
go func() {
http.Get(url)
close(ch)
}()
return ch
}
chan struct{}
を作成して返す関数ping
を定義しました。私たちのケースでは、チャネルに送信されるタイプを ケア するのではなく、完了したことを通知したいだけです。 チャネルを閉じることは完全に機能します!
なぜ
struct{}
で、bool
のような別の型ではないのですか?まあ、chan struct{}
はメモリの観点から利用できる最小のデータ型なので、bool
に対して割り当てはありません。 ちゃんと閉じて何も送信しないので、なぜ何かを割り当てるのですか?同じ関数内で、
http.Get(url)
を完了すると、そのチャネルに信号を送信するゴルーチン(goroutine
)を開始します。常にチャネルを作成する
チャネルを作成するときに、
make
を使用する方法に注意してください。 「var ch chan struct{}
」と言うのではなく。 var
を使用すると、変数は型の「ゼロ」値で初期化されます。したがって、string
の場合は""
、int
の場合は0
になります。チャネルの場合、ゼロ値は
nil
であり、<-
で送信しようとすると、nil
チャネルに送信できないため、永久にブロックされます。