How to test Go function containing log.Fatal()

25,974

Solution 1

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)

Solution 2

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.

Solution 3

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

Solution 4

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

Solution 5

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.

Share:
25,974
andrewsomething
Author by

andrewsomething

Ubuntu and Debian Developer DevEx @ DigitalOcean GitHub: andrewsomething Launchpad: andrewsomething IRC: asomething on Freenode Blog: http://blog.andrewsomething.com

Updated on November 18, 2020

Comments

  • andrewsomething
    andrewsomething over 3 years

    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)
        }
    }
    
  • Dave C
    Dave C about 9 years
    "Cannot" is a bit strong; but I'll agree with "should not". If you want to test a failure condition of a (non-main) block of code then don't use log.Fatal for that code but return an error instead (i.e. if failure is an option have an API that represents that). In the vast majority of cases log.Fatal should be only be used in main, or init functions (or possibly some things meant to be called only directly from them).
  • Plato
    Plato over 7 years
    in a complicated distributed system there are scenarios where my code cannot perform its task, for example a database went down or configuration is missing. i prefer to Fatal here, first to avoid executing with indeterminate program state and second to ensure i get a monitoring alert
  • voutasaurus
    voutasaurus almost 6 years
    Hey Plato, the answer you're referring to was my first answer. I removed it because I think it was encouraging people to be clever and also because somebody pointed out that it breaks the -cover flag, which is a fair point. Sorry I broke your reference. btw Andrew's slide is here and it contains all the relevant info: talks.golang.org/2014/testing.slide#23
  • MithunS
    MithunS over 4 years
    Agree halfway, it is not really necessary to have 100% code coverage, here the question at hand is more about how to go about unit testing when you have log.fatal in your code.
  • Bernie Lenz
    Bernie Lenz about 3 years
    Neat approach. The only catch with this is that you might encounter different errors in between calling logFatalf and when you check for "len(errors) != 1" as your code may now be in an invalid state after logFatalf but continues to run instead of aborting.