21

この質問: Go で os.exit シナリオをテストする方法(およびその中で最も投票された回答) は、os.Exit()go 内でシナリオをテストする方法を設定します。os.Exit()簡単に傍受できないため、バイナリを再度呼び出して終了値を確認する方法が使用されます。この方法については、Andrew Gerrand (Go チームのコア メンバーの 1 人) によるこのプレゼンテーションのスライド 23 で説明されています。コードは非常に単純で、以下に完全に再現されています。

関連するテスト ファイルとメイン ファイルは次のようになります(このファイルのペアだけが MVCE であることに注意してください)

package foo

import (
    "os"
    "os/exec"
    "testing"
)

func TestCrasher(t *testing.T) {
    if os.Getenv("BE_CRASHER") == "1" {
        Crasher() // This causes os.Exit(1) to be called
        return
    }
    cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
    cmd.Env = append(os.Environ(), "BE_CRASHER=1")
    err := cmd.Run()
    if e, ok := err.(*exec.ExitError); ok && !e.Success() {
        fmt.Printf("Error is %v\n", e)
    return
    }
    t.Fatalf("process ran with err %v, want exit status 1", err)
}

package foo

import (
    "fmt"
    "os"
)

// Coverage testing thinks (incorrectly) that the func below is
// never being called
func Crasher() {
    fmt.Println("Going down in flames!")
    os.Exit(1)
}

ただし、この方法には特定の制限があるようです。

  1. goveralls / coveralls.io を使用したカバレッジ テストは機能しません。たとえば、こちらの例(上記と同じコードですが、便宜上 github に配置されています) を参照してください。ここではカバレッジ テストが生成されます。つまり、実行中のテスト関数は記録されません。質問に答えるためにこれらのリンクは必要ないことに注意してください-上記の例はうまく機能します-上記をgithubに入れ、travisを介してcoveralls.ioにたどり着いた場合に何が起こるかを示すためだけです.

  2. テスト バイナリを再実行すると、壊れやすいように見えます。

具体的には、リクエストに応じて、カバレッジの失敗のスクリーンショット (リンクではなく) を以下に示します。赤い陰影は、coveralls.io に関する限り、Crasher()呼び出されていないことを示します。

Crasher() が呼び出されていないことを示すカバレッジ テスト

これを回避する方法はありますか?特に1点目。

golang レベルでの問題は次のとおりです。

  • Goveralls フレームワークが実行go test -cover ...され、上記のテストが呼び出されます。

  • 上記のテストは、OS 引数exec.Command / .Runなしで呼び出します-cover

  • etc. を無条件に-cover引数リストに入れるのは魅力的ではありません。これは、非カバレッジ テスト内で (サブプロセスとして) カバレッジ テストを実行することになり、引数リストを解析して-coveretc. の存在を確認することは、非常に重要な解決策のように思えます。

  • etc. を引数リストに入れても-cover、同じファイルに 2 つのカバレッジ出力が書き込まれ、うまくいきません。何らかの方法でマージする必要があります。私がそれに最も近いのは、この golang issueです。


概要

私が求めているのは、go カバレッジ テストを (できれば travis、goveralls、および coveralls.io を介して) 実行する簡単な方法であり、テストされたルーチンが で終了するテスト ケースと、OS.exit()そのテストのカバレッジが記録されているテスト ケースの両方が可能です。 . 上記の re-exec メソッドを使用することを強く希望します (それが機能する場合)。

ソリューションは、 のカバレッジ テストを表示する必要がありCrasher()ます。カバレッジテストから除外Crasher()することはオプションではありません.現実の世界では、私がやろうとしていることは、より複雑な関数をテストすることlog.Fatalf()です。私がカバレッジ テストを行っているのは、それらの条件のテストが適切に機能することです。

4

3 に答える 3

23

少しリファクタリングするだけで、簡単に 100% のカバレッジを実現できます。

foo/bar.go:

package foo

import (
    "fmt"
    "os"
)

var osExit = os.Exit

func Crasher() {
    fmt.Println("Going down in flames!")
    osExit(1)
}

そしてテストコード: foo/bar_test.go:

package foo

import "testing"

func TestCrasher(t *testing.T) {
    // Save current function and restore at the end:
    oldOsExit := osExit
    defer func() { osExit = oldOsExit }()

    var got int
    myExit := func(code int) {
        got = code
    }

    osExit = myExit
    Crasher()
    if exp := 1; got != exp {
        t.Errorf("Expected exit code: %d, got: %d", exp, got)
    }
}

実行中go test -cover:

Going down in flames!
PASS
coverage: 100.0% of statements
ok      foo        0.002s

os.Exit()はい、これは明示的に呼び出された場合に機能すると言うかもしれませんがos.Exit()、他の誰かによって呼び出された場合はどうなりlog.Fatalf()ますか?

同じテクニックがそこでも機能します。たとえば、log.Fatalf()の代わりに切り替える必要があります。os.Exit()

の関連部分foo/bar.go:

var logFatalf = log.Fatalf

func Crasher() {
    fmt.Println("Going down in flames!")
    logFatalf("Exiting with code: %d", 1)
}

そしてテストコード: TestCrasher()in foo/bar_test.go:

func TestCrasher(t *testing.T) {
    // Save current function and restore at the end:
    oldLogFatalf := logFatalf
    defer func() { logFatalf = oldLogFatalf }()

    var gotFormat string
    var gotV []interface{}
    myFatalf := func(format string, v ...interface{}) {
        gotFormat, gotV = format, v
    }

    logFatalf = myFatalf
    Crasher()
    expFormat, expV := "Exiting with code: %d", []interface{}{1}
    if gotFormat != expFormat || !reflect.DeepEqual(gotV, expV) {
        t.Error("Something went wrong")
    }
}

実行中go test -cover:

Going down in flames!
PASS
coverage: 100.0% of statements
ok      foo     0.002s
于 2016-11-25T09:36:53.113 に答える