web-dev-qa-db-ja.com

log.Fatal()を含むGo関数をテストする方法

たとえば、いくつかのログメッセージを出力する次のコードがあるとします。正しいメッセージがログに記録されていることをテストするにはどうすればよいですか? _log.Fatal_がos.Exit(1)を呼び出すと、テストは失敗します。

_package main

import (
    "log"
)

func hello() {
    log.Print("Hello!")
}

func goodbye() {
    log.Fatal("Goodbye!")
}

func init() {
    log.SetFlags(0)
}

func main() {
    hello()
    goodbye()
}
_

架空のテストは次のとおりです。

_package main

import (
    "bytes"
    "log"
    "testing"
)


func TestHello(t *testing.T) {
    var buf bytes.Buffer
    log.SetOutput(&buf)

    hello()

    wantMsg := "Hello!\n"
    msg := buf.String()
    if msg != wantMsg {
        t.Errorf("%#v, wanted %#v", msg, wantMsg)
    }
}

func TestGoodby(t *testing.T) {
    var buf bytes.Buffer
    log.SetOutput(&buf)

    goodbye()

    wantMsg := "Goodbye!\n"
    msg := buf.String()
    if msg != wantMsg {
        t.Errorf("%#v, wanted %#v", msg, wantMsg)
    }
}
_
24
andrewsomething

これは「 Goでのos.Exit()シナリオのテスト方法 "に似ています。デフォルトでlog.xxx()にリダイレクトする独自のロガーを実装する必要がありますが、テスト時に、log.Fatalf()のような関数を独自の関数(os.Exit(1)を呼び出さない)に置き換える機会

_exit/exit.go_os.Exit()呼び出しをテストするために同じことを行いました:

_exiter = New(func(int) {})
exiter.Exit(3)
So(exiter.Status(), ShouldEqual, 3)
_

(ここでは、私の「exit」関数は何もしない空の関数です)

11
VonC

Log.Fatalを含むコードをテストすることは可能ですが、お勧めできません。特に、-covergo testフラグでサポートされている方法でそのコードをテストすることはできません。

代わりに、log.Fatalを呼び出す代わりに、エラーを返すようにコードを変更することをお勧めします。シーケンシャル関数では、追加の戻り値を追加でき、ゴルーチンでは、タイプchan error(またはタイプエラーのフィールドを含む構造体タイプ)のチャネルでエラーを渡すことができます。

その変更が行われると、コードは非常に読みやすくなり、テストもはるかに簡単になり、移植性が向上します(コマンドラインツールに加えて、サーバープログラムで使用できるようになります)。

log.Println呼び出しがある場合、カスタムロガーをレシーバーのフィールドとして渡すこともお勧めします。そうすることで、サーバーのstderrまたはstdoutに設定できるカスタムロガーと、テストのnoopロガーにログを記録できます(テストで不要な出力が大量に取得されないようにします)。 logパッケージはカスタムロガーをサポートしているため、独自のロガーを作成したり、サードパーティのパッケージをインポートしたりする必要はありません。

8
voutasaurus

次のコードを使用して関数をテストしています。 xxx.go:

var logFatalf = log.Fatalf

if err != nil {
    logFatalf("failed to init launcher, err:%v", err)
}

そしてxxx_test.goで:

// TestFatal is used to do tests which are supposed to be fatal
func TestFatal(t *testing.T) {
    origLogFatalf := logFatalf

    // After this test, replace the original fatal function
    defer func() { logFatalf = origLogFatalf } ()

    errors := []string{}
    logFatalf = func(format string, args ...interface{}) {
        if len(args) > 0 {
            errors = append(errors, fmt.Sprintf(format, args))
        } else {
            errors = append(errors, format)
        }
    }
    if len(errors) != 1 {
        t.Errorf("excepted one error, actual %v", len(errors))
    }
}
8
clsung

logrus を使用している場合、v1.3.0からの終了関数を定義するオプションが this commit に導入されました。したがって、テストは次のようになります。

func Test_X(t *testing.T) {
    cases := []struct{
        param string
        expectFatal bool
    }{
        {
            param: "valid",
            expectFatal: false,
        },
        {
            param: "invalid",
            expectFatal: true,
        },
    }

    defer func() { log.StandardLogger().ExitFunc = nil }()
    var fatal bool
    log.StandardLogger().ExitFunc = func(int){ fatal = true }

    for _, c := range cases {
        fatal = false
        X(c.param)
        assert.Equal(t, c.expectFatal, fatal)
    }
}
7
Poh Zi How

私は非常に便利な bouk/monkey パッケージを使用します(ここでは stretchr/testify と一緒に)。

func TestGoodby(t *testing.T) {
  wantMsg := "Goodbye!"

  fakeLogFatal := func(msg ...interface{}) {
    assert.Equal(t, wantMsg, msg[0])
    panic("log.Fatal called")
  }
  patch := monkey.Patch(log.Fatal, fakeLogFatal)
  defer patch.Unpatch()
  assert.PanicsWithValue(t, "log.Fatal called", goodbye, "log.Fatal was not called")
}

このルートに進む前に bouk/monkeyを使用する際の注意 を読むことをお勧めします。

4
Allen Luce

以前は私が言及した答えがありましたが、削除されたようです。依存関係を変更したり、Fatalであるはずのコードを変更したりせずにテストに合格できるのは、これだけです。

これは通常不適切なテストであるという他の回答にも同意します。通常、テスト対象のコードを書き直してエラーを返し、エラーが期待どおりに返されることをテストします。nil以外のエラーを確認した後、より高いレベルのFatalをテストします。

正しいメッセージがログに記録されていることをテストするというOPの質問には、内部プロセスのcmd.Stdoutを検査します。

https://play.golang.org/p/J8aiO9_NoYS

func TestFooFatals(t *testing.T) {
    fmt.Println("TestFooFatals")
    outer := os.Getenv("FATAL_TESTING") == ""
    if outer {
        fmt.Println("Outer process: Spawning inner `go test` process, looking for failure from fatal")
        cmd := exec.Command(os.Args[0], "-test.run=TestFooFatals")
        cmd.Env = append(os.Environ(), "FATAL_TESTING=1")
        // cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
        err := cmd.Run()
        fmt.Printf("Outer process: Inner process returned %v\n", err)
        if e, ok := err.(*exec.ExitError); ok && !e.Success() {
            // fmt.Println("Success: inner process returned 1, passing test")
            return
        }
        t.Fatalf("Failure: inner function returned %v, want exit status 1", err)
    } else {
        // We're in the spawned process.
        // Do something that should fatal so this test fails.
        foo()
    }
}

// should fatal every time
func foo() {
    log.Printf("oh my goodness, i see %q\n", os.Getenv("FATAL_TESTING"))
    // log.Fatal("oh my gosh")
}
2
Plato

できませんし、すべきではありません。この「すべての行を「テスト」する必要があります」という態度は奇妙で、特に端末の状態ではそれがlog.Fatalの目的です。 (または単に外部からテストします。)

0
Volker