141

I'm currently pondering how to write tests that check if a given piece of code panicked? I know that Go uses recover to catch panics, but unlike say, Java code, you can't really specify what code should be skipped in case of a panic or what have you. So if I have a function:

func f(t *testing.T) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    OtherFunctionThatPanics()
    t.Errorf("The code did not panic")
}

I can't really tell whether OtherFunctionThatPanics panicked and we recovered, or if the function did not panic at all. How do I specify which code to skip over if there is no panic and which code to execute if there is a panic? How can I check whether there was some panic we recovered from?

030
  • 10,842
  • 12
  • 78
  • 123
ThePiachu
  • 8,695
  • 17
  • 65
  • 94

11 Answers11

171

testing doesn't really have the concept of "success," only failure. So your code above is about right. You might find this style slightly more clear, but it's basically the same thing.

func TestPanic(t *testing.T) {
    defer func() {
        if r := recover(); r == nil {
            t.Errorf("The code did not panic")
        }
    }()

    // The following is the code under test
    OtherFunctionThatPanics()
}

I generally find testing to be fairly weak. You may be interested in more powerful testing engines like Ginkgo. Even if you don't want the full Ginkgo system, you can use just its matcher library, Gomega, which can be used along with testing. Gomega includes matchers like:

Expect(OtherFunctionThatPanics).To(Panic())

You can also wrap up panic-checking into a simple function:

func TestPanic(t *testing.T) {
    assertPanic(t, OtherFunctionThatPanics)
}

func assertPanic(t *testing.T, f func()) {
    defer func() {
        if r := recover(); r == nil {
            t.Errorf("The code did not panic")
        }
    }()
    f()
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • @IgorMikushkin in Go 1.11, using the first form described by Rob Napier actually works for coverage. – FGM Dec 01 '18 at 11:18
  • 2
    Is there any reason you use `r := recover(); r == nil` and not just `recover() == nil`? – Duncan Jones Dec 20 '18 at 09:18
  • @DuncanJones Not really in this case. It's a really typical Go pattern to make the error available in the block, so it was probably habit for the OP to write it that way (and I brought his code forward), but it's not actually used in this case. – Rob Napier Dec 20 '18 at 13:17
85

If you use testify/assert, then it's a one-liner:

func TestOtherFunctionThatPanics(t *testing.T) {
  assert.Panics(t, OtherFunctionThatPanics, "The code did not panic")
}

Or, if your OtherFunctionThatPanics has a signature other than func():

func TestOtherFunctionThatPanics(t *testing.T) {
  assert.Panics(t, func() { OtherFunctionThatPanics(arg) }, "The code did not panic")
}

If you haven't tried testify yet, then also check out testify/mock. Super simple assertions and mocks.

Jacob Marble
  • 28,555
  • 22
  • 67
  • 78
41

Idiomatic Standard Library Solution

To me, the solution below is easy to read and shows a maintainer the natural code flow under test. Also, it doesn't require a third-party package.

func TestPanic(t *testing.T) {
    // No need to check whether `recover()` is nil. Just turn off the panic.
    defer func() { _ = recover() }()

    OtherFunctionThatPanics()

    // Never reaches here if `OtherFunctionThatPanics` panics.
    t.Errorf("did not panic")
}

For a more general solution, you can also do it like this:

func TestPanic(t *testing.T) {
    shouldPanic(t, OtherFunctionThatPanics)
}

func shouldPanic(t *testing.T, f func()) {
    t.Helper()
    defer func() { _ = recover() }()
    f()
    t.Errorf("should have panicked")
}

PS: _ = recover() is for satisfying the noisy linters that code does not check the error returned from the recover call—which is totally acceptable in this case.

Using a third-party package for testing in Go takes away the expressiveness of Go tests. It's like using a function to not to use if err != nil.

Inanc Gumus
  • 25,195
  • 9
  • 85
  • 101
  • 5
    Ah, time and again, "keep scrolling down" is the best motto for SO threads. Quite readable and zero-dependency, thanks! – kubanczyk Apr 04 '22 at 12:44
11

When looping over multiple test cases I would go for something like this:

package main

import (
    "reflect"
    "testing"
)


func TestYourFunc(t *testing.T) {
    type args struct {
        arg1 int
        arg2 int
        arg3 int
    }
    tests := []struct {
        name      string
        args      args
        want      []int
        wantErr   bool
        wantPanic bool
    }{
        //TODO: write test cases
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            defer func() {
                r := recover()
                if (r != nil) != tt.wantPanic {
                    t.Errorf("SequenceInt() recover = %v, wantPanic = %v", r, tt.wantPanic)
                }
            }()
            got, err := YourFunc(tt.args.arg1, tt.args.arg2, tt.args.arg3)
            if (err != nil) != tt.wantErr {
                t.Errorf("YourFunc() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("YourFunc() = %v, want %v", got, tt.want)
            }
        })
    }
}

Go playground

Aleh
  • 776
  • 7
  • 11
  • Conditions in tests increase the likelihood of bugs in those tests. There's absolutely no reason to have conditions in unit tests since all inputs are known as well as what their outputs are expected to be. IMNSHO, avoid such conditions at all costs. – Noel Yap Mar 02 '23 at 20:14
5

When you need to check the content of the panic, you can typecast the recovered value:

func TestIsAheadComparedToPanicsWithDifferingStreams(t *testing.T) {
    defer func() {
        err := recover().(error)

        if err.Error() != "Cursor: cannot compare cursors from different streams" {
            t.Fatalf("Wrong panic message: %s", err.Error())
        }
    }()

    c1 := CursorFromserializedMust("/foo:0:0")
    c2 := CursorFromserializedMust("/bar:0:0")

    // must panic
    c1.IsAheadComparedTo(c2)
}

If the code you're testing does not panic OR panic with an error OR panic with the error message you expect it to, the test will fail (which is what you'd want).

joonas.fi
  • 7,478
  • 2
  • 29
  • 17
  • 1
    It’s more robust to type-assert on a specific error type (e.g. os.SyscallError) than to compare error messages, which can change (e.g.) from one Go release to the next. – Michael Aug 09 '18 at 11:51
  • +Michael Aug, that's probably the better approach, for when there's a specific type available. – joonas.fi Aug 12 '18 at 21:25
4

Below is my panic expected

func TestPanic(t *testing.T) {

    panicF := func() {
        //panic call here
    }
    require.Panics(t, panicF)
}
Fabich
  • 2,768
  • 3
  • 30
  • 44
Nick L
  • 179
  • 1
  • 3
3

In your case you can do:

func f(t *testing.T) {
    recovered := func() (r bool) {
        defer func() {
            if r := recover(); r != nil {
                r = true
            }
        }()
        OtherFunctionThatPanics()
        // NOT BE EXECUTED IF PANICS
        // ....
    }
    if ! recovered() {
        t.Errorf("The code did not panic")

        // EXECUTED IF PANICS
        // ....
    }
}

As a generic panic router function this will also work:

https://github.com/7d4b9/recover

package recover

func Recovered(IfPanic, Else func(), Then func(recover interface{})) (recoverElse interface{}) {
    defer func() {
        if r := recover(); r != nil {
            {
                // EXECUTED IF PANICS
                if Then != nil {
                    Then(r)
                }
            }
        }
    }()

    IfPanic()

    {
        // NOT BE EXECUTED IF PANICS
        if Else != nil {
            defer func() {
                recoverElse = recover()
            }()
            Else()
        }
    }
    return
}

var testError = errors.New("expected error")

func TestRecover(t *testing.T) {
    Recovered(
        func() {
            panic(testError)
        },
        func() {
            t.Errorf("The code did not panic")
        },
        func(r interface{}) {
            if err := r.(error); err != nil {
                assert.Error(t, testError, err)
                return
            }
            t.Errorf("The code did an unexpected panic")
        },
    )
}
DBZ7
  • 59
  • 5
0

You can test which function paniced by giving panic an input

package main

import "fmt"

func explode() {
    // Cause a panic.
    panic("WRONG")
}

func explode1() {
    // Cause a panic.
    panic("WRONG1")
}

func main() {
    // Handle errors in defer func with recover.
    defer func() {
        if r := recover(); r != nil {
            var ok bool
            err, ok := r.(error)
            if !ok {
                err = fmt.Errorf("pkg: %v", r)
                fmt.Println(err)
            }
        }

    }()
    // These causes an error. change between these
    explode()
    //explode1()

    fmt.Println("Everything fine")

}

http://play.golang.org/p/ORWBqmPSVA

Thellimist
  • 3,757
  • 5
  • 31
  • 49
0

I would like to

  1. testPanic1 simple
  2. testPanic2 I prefer using this way because it is not enough to expect an error to occur. It should be precisely what the error is.
func testPanic1(testFunc func()) (isPanic bool) {
    defer func() {
        if err := recover(); err != nil {
            isPanic = true
        }
    }()
    testFunc()
    return false
}

func TestPanic() {
    fmt.Println(testPanic1(func() { panic("error...") })) // true
    fmt.Println(testPanic1(func() { fmt.Println("") }))   // false
}
func testPanic2(testFunc func()) (reason interface{}, isPanic bool) {
    defer func() {
        if err := recover(); err != nil {
            reason = err
            isPanic = true
        }
    }()
    testFunc()
    return nil, false
}

func TestPanic2() {
    reason, isPanic := testPanic2(func() { panic("my error") })
    fmt.Println(reason, isPanic) // "my error", true
    reason, isPanic = testPanic2(func() { fmt.Println("") })
    fmt.Println(reason, isPanic) // nil, false
}

More example

package _test

import (
    "fmt"
    "testing"
)

func testPanic(testFunc func()) (reason interface{}, isPanic bool) {
    defer func() {
        if err := recover(); err != nil {
            reason = err
            isPanic = true
        }
    }()
    testFunc()
    return nil, false
}

func TestPanicFunc(t *testing.T) {
    if reason, isPanic := testPanic(func() {
        panic("invalid memory address")
    }); !isPanic || reason != "invalid memory address" {
        t.Fatalf(`did not panic or panic msg != invalid memory address`)
    }

    if _, isPanic := testPanic(func() {
        _ = fmt.Sprintln("hello world")
    }); isPanic {
        t.Fatalf("It shouldn't cause panic.")
    }

    var ps *string
    if reason, isPanic := testPanic(func() {
        fmt.Print(*ps)
    }); !isPanic || reason.(error).Error() != "runtime error: invalid memory address or nil pointer dereference" {
        t.Fatalf(`did not panic or panic msg != "runtime error: invalid memory address or nil pointer dereference"`)
    }
}
Carson
  • 6,105
  • 2
  • 37
  • 45
0

strings_test.go in the standard library was adapted to produce the solution below.

// Example below has been adapted from a test in the Go Standard Library
// https://github.com/golang/go/blob/895664482c0ebe5cec4a6935615a1e9610bbf1e3/src/strings/strings_test.go#L1128-L1174
package main

import (
    "fmt"
    "strings"
    "testing"
)

// Wrapper function that recovers from panic
// in this case `strings.Repeat` is being recovered
func repeatWithRecover(s string, count int) (err error) {
    defer func() {
        if r := recover(); r != nil {
            switch v := r.(type) {
            case error:
                err = v
            default:
                err = fmt.Errorf("%s", v)
            }
        }
    }()
    // function that could panic goes here
    strings.Repeat(s, count)

    return
}

func TestRepeat(t *testing.T) {
    err := repeatWithRecover("a", -1)
    expected_err_str := "strings: negative Repeat count"
    if err == nil || !strings.Contains(err.Error(), expected_err_str) {
        t.Errorf("expected %q got %q", expected_err_str, err)
    }
}

https://go.dev/play/p/6oTB6DX421U

Mark
  • 1,337
  • 23
  • 34
0

Here's a simple example that builds on Rob's answer:

Consider a function counter accepts an integer and returns a pointer referencing to an integer:

  • main.go

    package main
    
    func counter(num int) *int {
        return &num
    }
    

And a test function:

  • main_test.go

    package main
    
    import "testing"
    
    func TestCounter(t *testing.T) {
        t.Run("de-referencing pointer to get number", func(t *testing.T) {
            var number int = 2
            got := counter(number)
            if *got != number {
                t.Errorf("Incorrect value: got %v, want %v", *got, number)
            }
        })
    
        t.Run("Runtime error expected", func(t *testing.T) {
            defer func() {
                if recover() == nil {
                    t.Error("The counter function did not panic")
                }
            }()
    
            var number *int
            nilPointer := counter(*number)
    
            t.Logf("Invalid memory address %v / Nil pointer de-reference %v", &nilPointer, *nilPointer)
        })
    }
    

In the first case we're de-referencing a pointer and comparing it with an integer, and in the second case we're testing panic.

The code below the function call nilPointer := counter(*number) will be skipped, i.e., t.Logf... will never execute.

The defer function will be executed at the end - in this case, after the function that's supposed to panic, panics. Recovery is checked by if recover() == nil, which indicates there was no recovery, even though the function call is supposed to cause a panic.

You would test for a panic inside this if block.

Saurabh
  • 5,176
  • 4
  • 32
  • 46