たとえば、いくつかのログメッセージを出力する次のコードがあるとします。正しいメッセージがログに記録されていることをテストするにはどうすればよいですか? _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)
}
}
_
これは「 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」関数は何もしない空の関数です)
Log.Fatalを含むコードをテストすることは可能ですが、お勧めできません。特に、-cover
のgo test
フラグでサポートされている方法でそのコードをテストすることはできません。
代わりに、log.Fatalを呼び出す代わりに、エラーを返すようにコードを変更することをお勧めします。シーケンシャル関数では、追加の戻り値を追加でき、ゴルーチンでは、タイプchan error
(またはタイプエラーのフィールドを含む構造体タイプ)のチャネルでエラーを渡すことができます。
その変更が行われると、コードは非常に読みやすくなり、テストもはるかに簡単になり、移植性が向上します(コマンドラインツールに加えて、サーバープログラムで使用できるようになります)。
log.Println
呼び出しがある場合、カスタムロガーをレシーバーのフィールドとして渡すこともお勧めします。そうすることで、サーバーのstderrまたはstdoutに設定できるカスタムロガーと、テストのnoopロガーにログを記録できます(テストで不要な出力が大量に取得されないようにします)。 log
パッケージはカスタムロガーをサポートしているため、独自のロガーを作成したり、サードパーティのパッケージをインポートしたりする必要はありません。
次のコードを使用して関数をテストしています。 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))
}
}
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)
}
}
私は非常に便利な 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を使用する際の注意 を読むことをお勧めします。
以前は私が言及した答えがありましたが、削除されたようです。依存関係を変更したり、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")
}
できませんし、すべきではありません。この「すべての行を「テスト」する必要があります」という態度は奇妙で、特に端末の状態ではそれがlog.Fatalの目的です。 (または単に外部からテストします。)