0
0

learn-go-with-tests Select

Posted at

Select

2つのURLを取得し、それらをHTTP GETでヒットして最初に返されたURLを返すことで「競合」するWebsiteRacerと呼ばれる関数を作成するように求められました。 10秒以内に戻らない場合は、「エラー(error)」を返します。

これには

  • HTTP呼び出しを行うための net/http
  • ゴルーチン
  • プロセスを同期するための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(slowURL, fastURL string) (winner string){
	return
}

これでテストを実行すると main_test.go:15: got "", want "http://www.quii.co.uk"とテストが走るようになります。

成功させるのに十分なコードを書く

では、テストが通るように修正します。
しかし、ここで問題なのはテストで本当のリクエストを飛ばすと外部サービスに依存してしまうためよくない方向です。そこで、標準ライブラリには、net/http/httptestというパッケージがあり、模擬HTTPサーバーを簡単に作成できるものがあるため、こちらを使用します。

テストをモックを使用するように変更して、制御できる信頼性の高いサーバーをテストできるようにします。

とりあえず遅いサーバーを用意します。(冗長ですが可読性を重視しました。)

	slowTestHandler := func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(20 * time.Millisecond) // 20ミリ秒の遅延
		w.WriteHeader(http.StatusOK)      // ステータスコード200を返す
	}
	slowHandler := http.HandlerFunc(slowTestHandler)
	slowServer := httptest.NewServer(slowHandler)

これだけだと初見ではよくわからないので詳しくみてみます。

NewServerの解説

まずNewServerの定義を見てみると以下のとおりです。
引数のhandlerhttp.Handlerのインターフェースを実装している必要があります。

func NewServer(handler http.Handler) *Server {
	ts := NewUnstartedServer(handler)
	ts.Start()
	return ts
}

具体的には、handlerServeHTTP(w http.ResponseWriter, r *http.Request)メソッドを持っている必要があります。

server.go
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

そこでhttp.HandlerFuncの定義を見てみると次の通りです。

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

つまり、以下のコードでは無名関数のtestHanlerHandlerFuncに型変換することでhttp.Handlerのインターフェースを実装していることとなります。

	testHandler := func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(20 * time.Millisecond) // 20ミリ秒の遅延
		w.WriteHeader(http.StatusOK)      // ステータスコード200を返す
	}
	// テスト用ハンドラを定義
	handler := http.HandlerFunc(testHandler)

つまり、handler.ServeHTTP(X, Y)と呼び出した時に、testHandler(X, Y)が呼び出されるという流れです。http.HandlerFuncがラッパーとして活躍しているわけです。

遠回りとなりましたが、このhandlerは先述したNewServerの中で以下のように使用され、起動したサーバーインスタンスを返す形となります。

func NewServer(handler http.Handler) *httptest.Server {
    // 新しいテストサーバーインスタンスを作成します(まだ起動していない)。
    ts := httptest.NewUnstartedServer(handler)
    // テストサーバーを起動します。
    ts.Start()
    // 起動したサーバーインスタンスを返します。
    return ts
}

この返却されたサーバーはServer構造体で次のようなフィールドを保持しています。

フィールド名 説明
URL string base URL of form http://ipaddr:port with no trailing slash
Listener net.Listener ローカルループバックインターフェース、エンドツーエンドHTTPテスト用
EnableHTTP2 bool HTTP/2がサーバーで有効かどうかを制御
TLS *tls.Config オプションのTLS構成、TLS開始後に新しい構成で設定
Config *http.Server NewUnstartedServer呼び出し後、StartまたはStartTLS呼び出し前に変更可能
certificate *x509.Certificate TLS構成証明書の解析バージョン
wg sync.WaitGroup サーバー上の未完了HTTPリクエスト数をカウント、Closeは全リクエスト完了までブロック
mu sync.Mutex closedとconnsを保護するためのミューテックス
closed bool サーバーが閉じられているかどうかを示すフラグ
conns map[net.Conn]http.ConnState HTTP接続の状態を保持(端末状態を除く)
client *http.Client サーバーで使用するために構成されたクライアント、Closeが呼び出されると自動的に閉じる

残りのテストを書く

次に早いサーバーを用意し、全体を修正します。

package Select

import (
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

func TestRacer(t *testing.T){
	slowTestHandler := func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(20 * time.Millisecond) // 20ミリ秒の遅延
		w.WriteHeader(http.StatusOK)      // ステータスコード200を返す
	}
	slowHandler := http.HandlerFunc(slowTestHandler)
	slowServer := httptest.NewServer(slowHandler)

	fastTestHandler := func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}
	fastHandler := http.HandlerFunc(fastTestHandler)
	fastServer := httptest.NewServer(fastHandler)
	
	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()
}

これでテストを実行すると、main_test.go:31: got "", want "http://127.0.0.1:50664"で失敗します。

テストがパスするようコードを修正

以下の通り修正します。

package Select

import (
	"net/http"
	"time"
)

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
}

リファクタリング

次に重複するコードがいくつかあるのでそちらをリファクタリングします。
こちらの修正ではmeasureReponseTime関数を用意し、処理を共通化します。

package Select

import (
	"net/http"
	"time"
)

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)
	Duration := time.Since(start)

	return Duration
}

次にテストでは以下の通りリファクタリングします。
サーバーインスタンスを返却する関数を作成することで共通処理をまとめることができます。また、サーバーをクローズする記述をdeferを使用することで見通しよくなります。

場合によっては、ファイルを閉じるなどのリソースをクリーンアップする必要があります。この場合、サーバーがポートをリッスンし続けないようにサーバーを閉じる必要があります。

これを関数の最後に実行したいが、将来のコードの読む人のために、サーバーを作成した場所の近くに命令を置いておきます。

package Select

import (
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

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)
		}),
	)
}

プロセスの同期

チャネルを使用してさらにリファクタリングします。

Racer関数では、リクエストしてから返却される具体的な時間を使用してレスポンスの速さを試していましたが、今回のどちらが早くレスポンスを返すかどうかというコンテクストでは、その時間は不要です。ただ、どちらが早いかさえ分かれば良いからです。

したがって、以下のようにチャネルを使用し書き換えることでよりシンプルな記述になります。

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
} 

select 文は複数のチャネル操作を同時に待機し、最初に完了した操作を実行します。
つまり、select 文内で ping(a) と ping(b) が同時に実行されるため、並行してリクエストを送信し、その完了を待つことができます。

  • case <-ping(a): ping(a) が返すチャネルが閉じられるのを待ちます。チャネルが閉じられた場合、このブロックが実行され、a を返します。
  • case <-ping(b): ping(b) が返すチャネルが閉じられるのを待ちます。チャネルが閉じられた場合、このブロックが実行され、b を返します。

したがって、どちらのレスポンスが早いかということを判別できるわけです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0