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

时间:2015-06-06 23:24:31

标签: testing 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)
    }
}

8 个答案:

答案 0 :(得分:9)

这类似于" How to test os.Exit() scenarios in Go":您需要实现自己的记录器,默认情况下会重定向到log.xxx(),但在测试时为您提供机会将log.Fatalf()这样的函数替换为您自己的函数(不调用os.Exit(1)

我在exit/exit.go中测试os.Exit()来电时做了同样的事情:

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

(这里,我的"退出"功能是空的,什么都不做)

答案 1 :(得分:7)

我使用以下代码来测试我的功能。在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))
    }
}

答案 2 :(得分:5)

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

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

一旦进行了更改,您的代码将更容易阅读,更容易测试,并且它将更易于移植(现在除了命令行工具之外,您还可以在服务器程序中使用它)。

如果您有log.Println个电话,我还建议您将自定义记录器作为字段传递到接收器上。这样您就可以登录到自定义记录器,您可以将其设置为服务器的stderr或stdout,以及用于测试的noop记录器(因此您不会在测试中获得大量不必要的输出)。 log包支持自定义记录器,因此不需要自己编写或导入第三方包。

答案 3 :(得分:1)

我会使用非常方便的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")
}

我建议在走这条路之前先阅读caveats to using bouk/monkey

答案 4 :(得分:1)

我曾经提到过这里的答案,看起来好像被删除了。这是我见过的唯一一个你可以通过测试而不修改依赖关系或以其他方式触及应该致命的代码的人。

我同意其他答案,这通常是不恰当的测试。通常你应该重写测试中的代码以返回错误,测试错误按预期返回,并在观察到非零错误后在更高级别范围内致命。

对于测试已记录正确消息的OP的问题,您将检查内部进程的select * from Emp e cross apply ( select top 1 ea.AddresId, a.AddressType from EmpAddress ea inner join Address a on ea.AddresId = a.AddresId where ea.EmpId = e.Id order by case a.AddressType when 'a' then 1 when 'c' then 2 when 'b' then 3 end ) a

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

cmd.Stdout

答案 5 :(得分:1)

如果您使用的是logrus,那么现在可以使用一个选项来定义this commit中引入的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)
    }
}

答案 6 :(得分:1)

我结合了不同来源的答案来得出这个结论:

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

效果很好:)

答案 7 :(得分:-1)

你不能也不应该。 这个"你必须测试'每一行" -attitude都很奇怪,特别是对于终端条件而言,这就是log.Fatal的用途。 (或者只是从外面测试它。)