エラーの種類

Error types

この章のすべてのコードはここにあります

エラー用に独自のタイプを作成することは、コードを整頓し、コードを使いやすくテストするための洗練された方法になる場合があります。

Gopher SlackPedroが尋ねる

fmt.Errorf("%s is foo、got%s"、bar、baz)のようなエラーを作成している場合、文字列値を比較せずに同等性をテストする方法はありますか?

このアイデアを探索するのに役立つ関数を作りましょう。

// DumbGetter will get the string body of url if it gets a 200
func DumbGetter(url string) (string, error) {
    res, err := http.Get(url)

    if err != nil {
        return "", fmt.Errorf("problem fetching from %s, %v", url, err)
    }

    if res.StatusCode != http.StatusOK {
        return "", fmt.Errorf("did not get 200 from %s, got %d", url, res.StatusCode)
    }

    defer res.Body.Close()
    body, _ := ioutil.ReadAll(res.Body) // ignoring err for brevity

    return string(body), nil
}

さまざまな理由で失敗する可能性のある関数を作成することは珍しいことではなく、各シナリオを正しく処理できるようにしたいと考えています。

Pedroが言うように、ステータスエラーのテストをそのように書くことができました。

t.Run("when you don't get a 200 you get a status error", func(t *testing.T) {

    svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
        res.WriteHeader(http.StatusTeapot)
    }))
    defer svr.Close()

    _, err := DumbGetter(svr.URL)

    if err == nil {
        t.Fatal("expected an error")
    }

    want := fmt.Sprintf("did not get 200 from %s, got %d", svr.URL, http.StatusTeapot)
    got := err.Error()

    if got != want {
        t.Errorf(`got "%v", want "%v"`, got, want)
    }
})

このテストでは、常にStatusTeapotを返すサーバーを作成し、そのURLをDumbGetterの引数として使用して、200以外の応答を正しく処理できることを確認します。

このテスト方法の問題

このサイトは テストに耳を傾ける ことを強調しようとしていますが、このテストは良いとは感じません。

  • テストのために本番コードと同じ文字列を作成しています

  • 読み書きが面倒

  • 正確なエラーメッセージ文字列は、実際に関係しているものですか?

これは何を教えてくれますか? テストの人間工学は、コードを使用しようとする別のコードに反映されます。

コードのユーザーは、返される特定の種類のエラーにどのように反応しますか? 彼らができる最善のことは、非常にエラーが発生しやすく、恐ろしいエラー文字列を調べることです。

私たちがすべきこと

TDDを使用すると、以下の考え方に入ることができます。

このコードをどのように使用したいですか?

DumbGetterにできることは、ユーザーが型システムを使用して発生したエラーの種類を理解する方法を提供することです。

もしもDumbGetterが次のようなものを返してくれたらどうでしょうか?

type BadStatusError struct {
    URL    string
    Status int
}

魔法の文字列ではなく、実際に使用する データ があります。

このニーズを反映するように既存のテストを変更しましょう。

t.Run("when you don't get a 200 you get a status error", func(t *testing.T) {

    svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
        res.WriteHeader(http.StatusTeapot)
    }))
    defer svr.Close()

    _, err := DumbGetter(svr.URL)

    if err == nil {
        t.Fatal("expected an error")
    }

    got, isStatusErr := err.(BadStatusError)

    if !isStatusErr {
        t.Fatalf("was not a BadStatusError, got %T", err)
    }

    want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot}

    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
})

BadStatusErrorにエラーインターフェースを実装させる必要があります。

func (b BadStatusError) Error() string {
    return fmt.Sprintf("did not get 200 from %s, got %d", b.URL, b.Status)
}

テストは何をしますか?

エラーの正確な文字列をチェックする代わりに、エラーに対してタイプアサーション(type assertion)を実行して、エラーが BadStatusErrorであるかどうかを確認しています。

これは、エラーをクリアするという種類の要望を反映しています。アサーションがパスすると仮定して、エラーのプロパティが正しいことを確認できます。

テストを実行すると、正しい種類のエラーが返されなかったことがわかります

--- FAIL: TestDumbGetter (0.00s)
    --- FAIL: TestDumbGetter/when_you_dont_get_a_200_you_get_a_status_error (0.00s)
        error-types_test.go:56: was not a BadStatusError, got *errors.errorString

タイプを使用するようにエラー処理コードを更新して、DumbGetterを修正しましょう

if res.StatusCode != http.StatusOK {
    return "", BadStatusError{URL: url, Status: res.StatusCode}
}

この変更は、いくつかの 現実的な プラスの効果をもたらしました。

  • DumbGetter関数がシンプルになりました。エラー文字列の複雑さに関係することはなくなり、BadStatusErrorを作成するだけです。

  • 私たちのテストは、コードのユーザーがロギングだけではなく、より高度なエラー処理を実行することを決定した場合に、およびドキュメントを反映しています。タイプアサーションを実行するだけで、エラーのプロパティに簡単にアクセスできます。

  • それでも「単なるエラー(error)」なので、彼らが選択した場合、コールスタックに渡すか、他の「エラーerror」と同様にログに記録できます。

まとめ

複数のエラー条件をテストしていることに気づいたら、エラーメッセージを比較するという罠にはまらないようにしてください。

これは、不完全で読み書きの難しいテストになります。また、発生したエラーの種類に応じて異なることを始める必要がある場合に、 コードのユーザが抱える困難さを反映します。

あなたがどのようにコードを使いたいかをテストに反映させるようにしてください。この点で、エラーの種類をカプセル化するためにエラータイプを作成することを検討してください。これにより、異なる種類のエラーの処理がコードのユーザにとって容易になり、また、エラー処理のコードをよりシンプルで読みやすく書くことができるようになります。

補遺

Go1.13では、標準ライブラリのエラーを扱う新しい方法があります。Goブログ

t.Run("when you don't get a 200 you get a status error", func(t *testing.T) {

    svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
        res.WriteHeader(http.StatusTeapot)
    }))
    defer svr.Close()

    _, err := DumbGetter(svr.URL)

    if err == nil {
        t.Fatal("expected an error")
    }

    var got BadStatusError
    isBadStatusError := errors.As(err, &got)
    want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot}

    if !isBadStatusError {
        t.Fatalf("was not a BadStatusError, got %T", err)
    }

    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
})

この場合、errors.Asを使ってエラーをカスタム型に抽出しています。これは成功を示すためにboolを返し、それをgotに抽出してくれます。

最終更新