如何测试包含log.Fatal()的Go函数

huangapple go评论78阅读模式
英文:

How to test Go function containing log.Fatal()

问题

以下是要翻译的内容:

假设我有以下打印一些日志消息的代码。我该如何测试是否已正确记录了这些消息?由于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)
    }
}
英文:

Say, I had the following code that prints some log messages. How would I go about testing that the correct messages have been logged? As log.Fatal calls os.Exit(1) the tests fail.

package main

import (
    "log"
)

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

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

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

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

Here are the hypothetical tests:

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)
    }
}

答案1

得分: 21

这类似于"如何在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"函数是一个空函数,什么都不做)

英文:

This is similar to "How to test os.Exit() scenarios in Go": you need to implement your own logger, which by default redirect to log.xxx(), but gives you the opportunity, when testing, to replace a function like log.Fatalf() with your own (which does not call os.Exit(1))

I did the same for testing os.Exit() calls in exit/exit.go:

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

(here, my "exit" function is an empty one which does nothing)

答案2

得分: 19

虽然可以测试包含log.Fatal的代码,但不建议这样做。特别是无法以go test命令的-cover标志支持的方式测试该代码。

相反,建议您将代码更改为返回错误而不是调用log.Fatal。在顺序函数中,您可以添加一个额外的返回值,在goroutine中,您可以通过类型为chan error的通道传递错误(或包含类型为error的字段的某个结构类型)。

一旦进行了这种更改,您的代码将更易于阅读,更易于测试,并且更具可移植性(现在您可以将其用于服务器程序以及命令行工具)。

如果您有log.Println调用,我还建议将自定义记录器作为接收器的字段传递。这样,您可以将日志记录到自定义记录器中,可以将其设置为stderr或stdout以供服务器使用,并在测试中使用noop记录器(这样您的测试中就不会有大量不必要的输出)。log包支持自定义记录器,因此无需编写自己的记录器或导入第三方包来实现此功能。

英文:

While it's possible to test code that contains log.Fatal, it is not recommended. In particular you cannot test that code in a way that is supported by the -cover flag on go test.

Instead it is recommended that you change your code to return an error instead of calling log.Fatal. In a sequential function you can add an additional return value, and in a goroutine you can pass an error on a channel of type chan error (or some struct type containing a field of type error).

Once that change is made your code will be much easier to read, much easier to test, and it will be more portable (now you can use it in a server program in addition to command line tools).

If you have log.Println calls I also recommend passing a custom logger as a field on a receiver. That way you can log to the custom logger, which you can set to stderr or stdout for a server, and a noop logger for tests (so you don't get a bunch of unnecessary output in your tests). The log package supports custom loggers, so there's no need to write your own or import a third party package for this.

答案3

得分: 16

如果你正在使用logrus,从v1.3.0版本开始,现在有一个选项可以定义你的退出函数,该选项在这个提交中引入。所以你的测试代码可能如下所示:

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)
    }
}
英文:

If you're using logrus, there's now an option to define your exit function from v1.3.0 introduced in this commit. So your test may look something like:

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)
	}
}

答案4

得分: 12

我已经使用以下代码来测试我的函数。在xxx.go文件中:

var logFatalf = log.Fatalf

if err != nil {
    logFatalf("初始化启动器失败,错误:%v", err)
}

在xxx_test.go文件中:

// TestFatal用于执行应该是致命的测试
func TestFatal(t *testing.T) {
    origLogFatalf := logFatalf

    // 在此测试之后,替换原始的致命函数
    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("期望一个错误,实际上有 %v 个错误", len(errors))
    }
}
英文:

I have using the following code to test my function. In xxx.go:

var logFatalf = log.Fatalf

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

And in 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))
    }
}

答案5

得分: 7

我会使用非常方便的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的注意事项

英文:

I'd use the supremely handy bouk/monkey package (here along with 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")
}

I advise reading the caveats to using bouk/monkey before going this route.

答案6

得分: 3

这里曾经有一个我参考过的答案,看起来被删除了。这是我见过的唯一一个在不修改依赖项或其他触及应该致命的代码的情况下能够通过测试的答案。

我同意其他答案的观点,通常这是一个不合适的测试。通常情况下,你应该重写被测试的代码以返回一个错误,测试该错误是否按预期返回,并在观察到非nil错误后在更高级别的范围内致命。

对于提问者关于测试正确的消息是否已被记录的问题,你可以检查内部进程的cmd.Stdout

以下是代码示例:

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")
}

希望对你有帮助!

英文:

There used to be an answer here that I referred to, looks like it got deleted. It was the only one I've seen where you could have passing tests without modifying dependencies or otherwise touching the code that should Fatal.

I agree with other answers that this is usually an inappropriate test. Usually you should rewrite the code under test to return an error, test the error is returned as expected, and Fatal at a higher level scope after observing the non-nil error.

To OP's question of testing that the that the correct messages have been logged, you would inspect inner process's 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")
}

答案7

得分: 2

我已经翻译好了你提供的代码部分:

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"os/user"
	"strings"
	"testing"

	"bou.ke/monkey"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

func TestCommandThatErrors(t *testing.T) {
	fakeExit := func(int) {
		panic("os.Exit called")
	}
	patch := monkey.Patch(os.Exit, fakeExit)
	defer patch.Unpatch()

	var buf bytes.Buffer
	log.SetOutput(&buf)

	for _, tc := range []struct {
		cliArgs       []string
		expectedError string
	}{
		{
			cliArgs:       []string{"dev", "api", "--dockerless"},
			expectedError: "Some services don't have dockerless variants implemented yet.",
		},
	} {
		t.Run(strings.Join(tc.cliArgs, " "), func(t *testing.T) {
			harness := createTestApp()
			for _, cmd := range commands {
				cmd(harness.app)
			}

			assert.Panics(t, func() { harness.app.run(tc.cliArgs) })
			assert.Contains(t, buf.String(), tc.expectedError)
			buf.Reset()
		})
	}
}

非常好 如何测试包含log.Fatal()的Go函数

英文:

I've combined answers from different sources to produce this:

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"os/user"
	"strings"
	"testing"

	"bou.ke/monkey"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)


func TestCommandThatErrors(t *testing.T) {
	fakeExit := func(int) {
		panic("os.Exit called")
	}
	patch := monkey.Patch(os.Exit, fakeExit)
	defer patch.Unpatch()

	var buf bytes.Buffer
	log.SetOutput(&buf)

	for _, tc := range []struct {
		cliArgs       []string
		expectedError string
	}{
		{
			cliArgs:       []string{"dev", "api", "--dockerless"},
			expectedError: "Some services don't have dockerless variants implemented yet.",
		},
	} {
		t.Run(strings.Join(tc.cliArgs, " "), func(t *testing.T) {
			harness := createTestApp()
			for _, cmd := range commands {
				cmd(harness.app)
			}

			assert.Panics(t, func() { harness.app.run(tc.cliArgs) })
			assert.Contains(t, buf.String(), tc.expectedError)
			buf.Reset()
		})
	}
}

Works great 如何测试包含log.Fatal()的Go函数

答案8

得分: -2

你不能也不应该这样做。
这种“你必须‘测试’每一行代码”的态度很奇怪,尤其是对于终端条件,这就是log.Fatal的作用所在。
(或者只需从外部进行测试。)

英文:

You cannot and you should not.
This "you must 'test' each and every line"-attitude is strange, especially for terminal conditions and that's what log.Fatal is for.
(Or just test it from the outside.)

huangapple
  • 本文由 发表于 2015年6月7日 07:24:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/30688554.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定