Goにおけるダイナミックドメイン変数

5476 ワード

これはAPI設計の思想実験であり、典型的なGoユニットテストの慣用形式から始まる.
func TestOpenFile(t *testing.T) {
        f, err := os.Open("notfound")
        if err != nil {
                t.Fatal(err)
        }

        // ...
}

このコードに何か問題がありますか?断言if err != nil { ... }が重複しており、複数の条件をチェックする必要がある場合、テストの作成者がt.Errorではなくt.Fatalを使用すると、エラーが発生しやすくなります.たとえば、次のようになります.
f, err := os.Open("notfound")
        if err != nil {
                t.Error(err)
        }
        f.Close() // boom!

どんな解決策がありますか.もちろん、繰り返しの断言論理を補助関数に移動することによってDRY(Don't Repeat Yourself)に達する.
func TestOpenFile(t *testing.T) {
        f, err := os.Open("notfound")
        check(t, err)

        // ...
}

func check(t *testing.T, err error) {
       if err != nil {
                t.Helper()
                t.Fatal(err)
        }
}
check補助関数を使用すると、このコードがより簡潔になり、エラーがより明確にチェックされ、t.Errort.Fatalの混同使用を解決することが期待される.断言を補助関数として抽象化する欠点は、testing.Tを各呼び出しに渡す必要があることです.さらに悪いことに、万が一に備えて、*testing.Tcheckを呼び出す必要がある場所に伝える必要があります.
これは関係ないと思います.しかし、失敗を断言するときにのみ変数tが使用されることが観察されます.テストシーンでも、ほとんどのテストが合格するため、比較的珍しいテストに失敗した場合、これらの変数tに対する固定読み書きオーバーヘッドが発生します.
もし私たちがこのようにしたらどうですか?
func TestOpenFile(t *testing.T) {
        f, err := os.Open("notfound")
        check(err)

        // ...
}

func check(err error) {
        if err != nil {
                panic(err.Error())
        }
}

はい、いいですが、いくつか問題があります.
% go test
--- FAIL: TestOpenFile (0.00s)
panic: open notfound: no such file or directory [recovered]
        panic: open notfound: no such file or directory

goroutine 22 [running]:
testing.tRunner.func1(0xc0000b4400)
        /Users/dfc/go/src/testing/testing.go:874 +0x3a3
panic(0x111b040, 0xc0000866f0)
        /Users/dfc/go/src/runtime/panic.go:679 +0x1b2
github.com/pkg/expect_test.check(...)
        /Users/dfc/src/github.com/pkg/expect/expect_test.go:18
github.com/pkg/expect_test.TestOpenFile(0xc0000b4400)
        /Users/dfc/src/github.com/pkg/expect/expect_test.go:10 +0xa1
testing.tRunner(0xc0000b4400, 0x115ac90)
        /Users/dfc/go/src/testing/testing.go:909 +0xc9
created by testing.(*T).Run
        /Users/dfc/go/src/testing/testing.go:960 +0x350
exit status 2

まず、良い側面から言えば、testing.Tを各呼び出しcheck関数に渡す必要はありません.テストはすぐに失敗します.私たちはpanicから2回繰り返したにもかかわらず、良い情報を得ました.しかし、失敗をどこで断言するかは容易には見えない.expect_test.go:11で発生しましたが、それは許せないことを知っています.
だからpanicは良い解決策ではありませんが、スタック追跡情報から何か役に立つ情報を見ることができますか?これには、github.com/pkg/expect_test.TestOpenFile(0xc0000b4400)というヒントがあります.
TestOpenFileはtRunnerによって渡されるtの値があるのでtesting.Tは、アドレス0 xc 0000 b 4400上にメモリ内に存在する.check関数の内部でtを取得できたらどうなりますか?では、t.Helperをt.Fatalに呼び出すことができます.それは可能ですか?

ダイナミックドメイン


グローバル範囲でも関数ローカル範囲でもなく、呼び出しスタックのより高い位置にある変数にアクセスできることを示したい.これを動的役割ドメインと呼ぶ.Goは動的役割ドメインをサポートしていないが,場合によってはシミュレーションできることが実証されている.本題に戻ります.
// getT   testing.tRunner   testing.T  
//   getT  (tRunner) .   testing.tRunner
//   getT   goroutine  ,
//   getT   nil.
func getT() *testing.T {
        var buf [8192]byte
        n := runtime.Stack(buf[:], false)
        sc := bufio.NewScanner(bytes.NewReader(buf[:n]))
        for sc.Scan() {
                var p uintptr
                n, _ := fmt.Sscanf(sc.Text(), "testing.tRunner(%v", &p)
                if n != 1 {
                        continue
                }
                return (*testing.T)(unsafe.Pointer(p))
        }
        return nil
}

各テスト(Test)はtestingパッケージによって自分のgoroutine上で呼び出されることを知っています(上のスタック情報を見てください).testingパッケージは、testingを必要とするtRunnerという関数によってテストを開始する.Tとfunc(testing.T)が呼び出される.そこで、現在のgoroutineのスタック情報をキャプチャし、スキャンしてtestingを見つけました.tRunnerの先頭の行--tRunnerはプライベート関数であるため、testingパケットしかない--最初のパラメータのアドレスを解析し、このアドレスはtestingを指す.Tの針.少し安全ではありません.この元のポインタを*testingに変換します.T私たちは完成しました.
検索できない場合はgettがTestによって呼び出されていない可能性があります.これは実際には通用します.私たちは*testingが必要です.Tはt.Fatalを呼び出すためであり、testingパケットはt.Fatalがメインテストgoroutineに呼び出されることを要求する.
import "github.com/pkg/expect"

func TestOpenFile(t *testing.T) {
        f, err := os.Open("notfound")
        expect.Nil(err)

        // ...
}

以上,ファイルを開くと予想されるerrがnilになった後,断言テンプレートを除去し,テストがより明瞭で読みやすいように見えた.

これでいいですか。


この時、あなたは聞くはずです.これでいいですか.答えは、いいえ、これはよくありません.この時あなたは驚くべきですが、これらの悪い感じは反省に値するかもしれません.goroutineの呼び出しスタックで逃げ回る固有の不十分さに加えて、同様にいくつかの深刻な設計問題があります.
  • expect.Nilの動作は誰がそれを呼び出すかに依存する.同じパラメータでは、スタック位置を呼び出すことによって動作が異なる可能性があります.これは予想できません.
  • は、単一の関数に渡される前のすべての関数のすべての変数を単一の関数の役割ドメインに組み入れる極端な動的役割ドメインをとる.これは,関数が明確に記録されていないことを明らかにした場合にデータを転送および転送する補助手段である.

  • 皮肉なことに、これはちょうど私がcontextに対してです.Contextの評価.私はこの問題をあなた自身に残して合理的かどうかを判断します.

    最後の言葉


    これは悪い考えだ,これには異議はない.これはあなたが生産モードで使用できるモードではありません.しかし、これも生産コードではありません.これはテストです.テストコードに適用されるルールが異なるかもしれません.結局、シミュレーション(mocks)、杭(stubs)、サルパッチ(monkey patching)、タイプ断言、反射、補助関数、構築フラグ、グローバル変数を使用して、コードのテストをより効率的に行います.これらすべて、奇技淫巧は生産コードに現れないので、これは本当に世界の終わりですか?
    もしあなたが本文を読み終わったら、あなたは私の観点に同意するかもしれませんが、あまり慣行に合わないにもかかわらず、*testingする必要はありません.Tは,断言を必要とするすべての関数に伝達され,テストコードをより明確にする.
    興味があれば、このモードを適用する小さな断言ライブラリを共有しました.注意して使用してください.
    via: https://studygolang.com/subject/1?p=1
    作者:Dave Cheney訳者:dust 347校正:unknwon
    本文はGCTTオリジナルコンパイル、[Go言語中国語網]