Goアプリのテスト:正しい方法で始めましょう
公開: 2022-03-11何か新しいことを学ぶときは、新鮮な心の状態を持つことが重要です。
Goにかなり慣れておらず、JavaScriptやRubyなどの言語を使用している場合は、モック、アサーション、およびその他のテストウィザードを実行するのに役立つ既存のフレームワークを使用することに慣れている可能性があります。
今度は、外部の依存関係やフレームワークに依存するという考えを根絶してください! テストは、数年前、利用可能なリソースがはるかに少なかった時代に、この注目に値するプログラミング言語を学んだときに私が遭遇した最初の障害でした。
Goで成功をテストするということは、(すべてのGoと同様に)依存関係を軽視し、外部ライブラリに最小限に依存し、再利用可能な優れたコードを作成することを意味することを今では知っています。 サードパーティのテストライブラリを使って冒険したBlakeMizeranyの経験のこのプレゼンテーションは、考え方を調整するための素晴らしいスタートです。 外部ライブラリとフレームワークを使用することと、それを「ゴーウェイ」で実行することについて、いくつかの良い議論があります。
独自のテストフレームワークとモックの概念を構築することは直感に反するように思えるかもしれませんが、想像するよりも簡単であり、言語を学ぶための良い出発点です。 さらに、私が学んでいたときとは異なり、この記事では、一般的なテストシナリオをガイドし、コードを効率的にテストしてクリーンに保つためのベストプラクティスを検討する手法を紹介します。
Goでのテーブルテスト
「ユニットテスト」の名声の基本的なテストユニットは、入力を受け取り、出力を返す最も単純な形式のプログラムの任意のコンポーネントにすることができます。 テストを作成したい単純な関数を見てみましょう。 完璧にも完全にもほど遠いですが、デモンストレーションの目的には十分です。
avg.go
func Avg(nos ...int) int { sum := 0 for _, n := range nos { sum += n } if sum == 0 { return 0 } return sum / len(nos) }
上記の関数func Avg(nos ...int)
は、ゼロまたは与えられた一連の数値の整数平均を返します。 それでは、テストを書いてみましょう。
Goでは、テスト対象のコードを含むファイルと同じ名前に、接尾辞_test
を追加して、テストファイルに名前を付けることがベストプラクティスと見なされます。 たとえば、上記のコードはavg.go
という名前のファイルにあるため、テストファイルの名前はavg_test.go
になります。
簡単にするためにパッケージの定義とインポートは省略されているため、これらの例は実際のファイルの抜粋にすぎないことに注意してください。
Avg
関数のテストは次のとおりです。
avg__test.go
func TestAvg(t *testing.T) { for _, tt := range []struct { Nos []int Result int }{ {Nos: []int{2, 4}, Result: 3}, {Nos: []int{1, 2, 5}, Result: 2}, {Nos: []int{1}, Result: 1}, {Nos: []int{}, Result: 0}, {Nos: []int{2, -2}, Result: 0}, } { if avg := Average(tt.Nos...); avg != tt.Result { t.Fatalf("expected average of %v to be %d, got %d\n", tt.Nos, tt.Result, avg) } } }
関数の定義について注意すべき点がいくつかあります。
- まず、テスト関数名の「Test」のプレフィックス。 これは、ツールが有効なテストとしてそれを取得するために必要です。
- 関数名の後半部分は、通常、テストされる関数またはメソッドの名前です。この場合は
Avg
です。 - また、
testing.T
と呼ばれるテスト構造を渡す必要があります。これにより、テストのフローを制御できます。 このAPIの詳細については、ドキュメントページをご覧ください。
それでは、例が書かれている形式について話しましょう。 テストスイート(一連のテスト)は関数Avg()
を介して実行されており、各テストには特定の入力と期待される出力が含まれています。 この場合、各テストは整数のスライス( Nos
)を送信し、特定の戻り値( Result
)を期待します。
Golangインターフェースのモック
Go言語が提供しなければならない最大かつ最も強力な機能の1つは、インターフェースと呼ばれます。 プログラムを設計するときにインターフェースから得られるパワーと柔軟性に加えて、インターフェースは、コンポーネントを切り離し、それらのミーティングポイントで徹底的にテストする素晴らしい機会も提供します。
io.Readerから最初のNバイトを読み取り、それらを文字列として返す必要がある架空のシナリオを考えてみましょう。 次のようになります。
readn.go
// readN reads at most n bytes from r and returns them as a string. func readN(r io.Reader, n int) (string, error) { buf := make([]byte, n) m, err := r.Read(buf) if err != nil { return "", err } return string(buf[:m]), nil }
明らかに、テストする主なことは、関数readN
がさまざまな入力を与えられると、正しい出力を返すことです。 これは、テーブルテストで実行できます。 しかし、私たちがカバーすべき他の2つの重要な側面があり、それは次のことをチェックしています。
-
r.Read
は、サイズnのバッファーで呼び出されます。 -
r.Read
がスローされると、エラーが返されます。
r.Read
に渡されるバッファーのサイズを知り、それが返すエラーを制御するには、 readN
に渡されるr
をモックする必要があります。 タイプReaderのGoドキュメントを見ると、 io.Reader
がどのように見えるかがわかります。
type Reader interface { Read(p []byte) (n int, err error) }
それはかなり簡単なようです。 io.Reader
を満たすために必要なのは、モックにRead
メソッドを持たせることだけです。 したがって、 ReaderMock
は次のようになります。
type ReaderMock struct { ReadMock func([]byte) (int, error) } func (m ReaderMock) Read(p []byte) (int, error) { return m.ReadMock(p) }
上記のコードを少し分析してみましょう。 ReaderMock
のインスタンスは、必要なRead
メソッドを実装しているため、 io.Reader
インターフェイスを明確に満たしています。 モックにはフィールドReadMock
も含まれているため、モックメソッドの正確な動作を設定できます。これにより、必要なものを動的にインスタンス化することが非常に簡単になります。
実行時にインターフェースが満たされるようにするための優れたメモリーフリーのトリックは、コードに以下を挿入することです。
var _ io.Reader = (*MockReader)(nil)
これはアサーションをチェックしますが、何も割り当てません。これにより、プログラムが実際にそれを使用する機能に実行される前に、コンパイル時にインターフェイスが正しく実装されていることを確認できます。 オプションのトリックですが、役に立ちます。
次に、最初のテストを作成します。このテストでは、サイズn
のバッファーを使用してr.Read
が呼び出されます。 これを行うには、 ReaderMock
を次のように使用します。
func TestReadN_bufSize(t *testing.T) { total := 0 mr := &MockReader{func(b []byte) (int, error) { total = len(b) return 0, nil }} readN(mr, 5) if total != 5 { t.Fatalf("expected 5, got %d", total) } }
上記のように、スコープ変数を使用して「偽の」 io.Reader
のRead
関数の動作を定義しました。これは、後でテストの有効性を主張するために使用できます。 簡単です。
テストする必要がある2番目のシナリオを見てみましょう。このシナリオでは、 Read
をモックしてエラーを返す必要があります。
func TestReadN_error(t *testing.T) { expect := errors.New("some non-nil error") mr := &MockReader{func(b []byte) (int, error) { return 0, expect }} _, err := readN(mr, 5) if err != expect { t.Fatal("expected error") } }
上記のテストでは、 mr.Read
(モックリーダー)を呼び出すと、定義されたエラーが返されるため、 readN
が正しく機能しても同じことが行われると想定しても問題ありません。
Goによる関数モック
代わりに構造体とインターフェースを使用する傾向があるため、関数をモックする必要があることはめったにありません。 これらは制御が簡単ですが、ときどきこの必要性にぶつかることがあり、このトピックに関して混乱が頻繁に見られます。 log.Println
のようなものをモックする方法を尋ねる人さえいます。 log.Println
に与えられた入力をテストする必要があることはめったにありませんが、この機会を利用してデモンストレーションを行います。
n
の値に応じて出力をログに記録する以下の単純なif
ステートメントを検討してください。
func printSize(n int) { if n < 10 { log.Println("SMALL") } else { log.Println("LARGE") } }
上記の例では、 log.Println
が正しい値で呼び出されることを具体的にテストするというばかげたシナリオを想定しています。 この関数をモックするには、最初に関数を自分の関数の中にラップする必要があります。

var show = func(v ...interface{}) { log.Println(v...) }
この方法で(変数として)関数を宣言すると、テストで関数を上書きし、必要な動作を割り当てることができます。 暗黙的に、 log.Println
を参照する行はshow
に置き換えられるため、プログラムは次のようになります。
func printSize(n int) { if n < 10 { show("SMALL") } else { show("LARGE") } }
これで、テストできます。
func TestPrintSize(t *testing.T) { var got string oldShow := show show = func(v ...interface{}) { if len(v) != 1 { t.Fatalf("expected show to be called with 1 param, got %d", len(v)) } var ok bool got, ok = v[0].(string) if !ok { t.Fatal("expected show to be called with a string") } } for _, tt := range []struct{ N int Out string }{ {2, "SMALL"}, {3, "SMALL"}, {9, "SMALL"}, {10, "LARGE"}, {11, "LARGE"}, {100, "LARGE"}, } { got = "" printSize(tt.N) if got != tt.Out { t.Fatalf("on %d, expected '%s', got '%s'\n", tt.N, tt.Out, got) } } // careful though, we must not forget to restore it to its original value // before finishing the test, or it might interfere with other tests in our // suite, giving us unexpected and hard to trace behavior. show = oldShow }
私たちの持ち帰りは' log.Println
'であってはなりませんが、正当な理由でパッケージレベルの関数をモックする必要がある非常にまれなシナリオでは、(私が知る限り)それを行う唯一の方法はそれをパッケージレベルの変数として宣言し、その値を制御できるようにします。
ただし、 log.Println
のようなものをモックする必要がある場合は、カスタムロガーを使用すると、はるかに洗練されたソリューションを作成できます。
テンプレートレンダリングテストに進む
もう1つのかなり一般的なシナリオは、レンダリングされたテンプレートの出力が期待どおりであることをテストすることです。 http://localhost:3999/welcome?name=Frank
へのGETリクエストについて考えてみましょう。これにより、次の本文が返されます。
<html> <head><title>Welcome page</title></head> <body> <h1 class="header-name"> Welcome <span class="name">Frank</span>! </h1> </body> </html>
今のところ十分に明らかではなかった場合、クエリパラメータname
が「名前」として分類されたspan
の内容と一致するのは偶然ではありません。 この場合、明らかなテストは、これが複数の出力にわたって毎回正しく行われることを確認することです。 ここでは、GoQueryライブラリが非常に役立つことがわかりました。
これで、次の方法でテストを記述できます。
welcome__test.go
func TestWelcome_name(t *testing.T) { resp, err := http.Get("http://localhost:3999/welcome?name=Frank") if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } doc, err := goquery.NewDocumentFromResponse(resp) if err != nil { t.Fatal(err) } if v := doc.Find("h1.header-name span.name").Text(); v != "Frank" { t.Fatalf("expected markup to contain 'Frank', got '%s'", v) } }
まず、先に進む前に、応答コードが200/OKであることを確認します。
上記のコードスニペットの残りの部分が自明であると想定するのはそれほど難しいことではないと思いますhttp
パッケージを使用してURLを取得し、応答から新しいgoquery互換ドキュメントを作成します。これを使用してクエリを実行します。返されたDOM。 h1.header-name
内のspan.name
がテキスト「Frank」をカプセル化していることを確認します。
JSONAPIのテスト
Goは、ある種のAPIを作成するために頻繁に使用されるため、最後になりましたが、JSONAPIをテストするいくつかの高レベルの方法を見てみましょう。
エンドポイントが以前にHTMLではなくJSONを返したかどうかを検討してください。したがって、 http://localhost:3999/welcome.json?name=Frank
から、応答の本文は次のようになります。
{"Salutation": "Hello Frank!"}
すでに推測しているように、JSON応答のアサートは、外部ライブラリや依存関係が必要ないことを除いて、テンプレート応答のアサートと大差ありません。 Goの標準ライブラリで十分です。 これは、指定されたパラメーターに対して正しいJSONが返されることを確認するテストです。
welcome__test.go
func TestWelcome_name_JSON(t *testing.T) { resp, err := http.Get("http://localhost:3999/welcome.json?name=Frank") if err != nil { t.Fatal(err) } if resp.StatusCode != 200 { t.Fatalf("expected 200, got %d", resp.StatusCode) } var dst struct{ Salutation string } if err := json.NewDecoder(resp.Body).Decode(&dst); err != nil { t.Fatal(err) } if dst.Salutation != "Hello Frank!" { t.Fatalf("expected 'Hello Frank!', got '%s'", dst.Salutation) } }
デコードする構造以外のものが返される場合、 json.NewDecoder
は代わりにエラーを返し、テストは失敗します。 応答が構造に対して正常にデコードされることを考慮して、フィールドの内容が期待どおりであることを確認します。この場合は「HelloFrank!」です。
セットアップと分解
Goを使用したテストは簡単ですが、上記のJSONテストとその前のテンプレートレンダリングテストの両方に1つの問題があります。 どちらもサーバーが実行されていることを前提としているため、信頼性の低い依存関係が作成されます。 また、「ライブ」サーバーに反対するのは良い考えではありません。
幸い、Goはテストサーバーを作成するためのhttptestパッケージを提供しています。 テストは、メインサーバーから独立した独自のサーバーを起動するため、テストが本番環境に干渉することはありません。
このような場合、実行中のサーバーを必要とするすべてのテストで呼び出される汎用のsetup
およびteardown
関数を作成するのが理想的です。 この新しい、より安全なパターンに従うと、テストは次のようになります。
func setup() *httptest.Server { return httptest.NewServer(app.Handler()) } func teardown(s *httptest.Server) { s.Close() } func TestWelcome_name(t *testing.T) { srv := setup() url := fmt.Sprintf("%s/welcome.json?name=Frank", srv.URL) resp, err := http.Get(url) // verify errors & run assertions as usual teardown(srv) }
app.Handler()
参照に注意してください。 これは、アプリケーションのhttp.Handlerを返すベストプラクティス関数であり、本番サーバーまたはテストサーバーのいずれかをインスタンス化できます。
結論
Goでのテストは、プログラムの外側の視点を想定し、訪問者、またはほとんどの場合、APIのユーザーの立場に立つ絶好の機会です。 これは、優れたコードと高品質のエクスペリエンスの両方を提供していることを確認する絶好の機会を提供します。
コードのより複雑な機能がわからない場合は、テストが安心として役立ちます。また、大規模なシステムの一部を変更するときに、これらの要素が引き続き適切に機能することを保証します。
この記事がお役に立てば幸いです。他のテストのコツをご存知の場合は、コメントをお待ちしています。