如何测试恐慌?


90

我目前正在考虑如何编写测试,以检查给定的代码是否发生了恐慌?我知道Go曾经recover用来捕获恐慌,但是与说Java代码不同,您不能真正指定在发生恐慌时应该跳过哪些代码或您有什么问题。因此,如果我有一个功能:

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

我真的不能说是OtherFunctionThatPanics惊慌失措而我们恢复了,还是该函数根本没有惊慌。如果没有恐慌,我该如何指定跳过哪些代码,如果有恐慌,我应如何执行?如何检查我们是否从中恢复了恐慌?

Answers:


106

testing并没有真正的“成功”概念,只有失败。因此,您上面的代码是正确的。您可能会发现这种样式稍微清晰一些,但是基本上是同一回事。

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

我通常觉得testing自己很弱。您可能对更强大的测试引擎(例如Ginkgo)感兴趣。即使您不需要完整的Ginkgo系统,也可以仅使用其匹配器库Gomega,该库可以与一起使用testing。Gomega包括以下匹配项:

Expect(OtherFunctionThatPanics).To(Panic())

您还可以将恐慌检查包装成一个简单的函数:

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

Go 1.11中的@IgorMikushkin使用Rob Napier所描述的第一种形式来实际覆盖。
FGM

您有使用任何理由r := recover(); r == nil而不仅仅是recover() == nil吗?
Duncan Jones

@DuncanJones在这种情况下不是真的。这是使错误在块中可用的一种非常典型的Go模式,因此OP习惯以这种方式编写错误(并且我将他的代码提出来),但在这种情况下并未实际使用。
罗布·纳皮尔

43

如果您使用testify / assert,那么它是单线的:

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

或者,如果您OtherFunctionThatPanics的签名不是func()

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

如果您还没有尝试作证,那么也请查看testify / mock。超级简单的断言和模拟。


7

当遍历多个测试用例时,我会选择这样的东西:

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

去操场


4

当需要检查紧急情况的内容时,可以键入恢复值:

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

如果您要测试的代码没有引起恐慌或出现错误而出现恐慌,或者没有出现预期的错误消息而导致恐慌,则测试将失败(这是您想要的)。


1
对特定错误类型(例如os.SyscallError)进行类型断言比对错误消息进行比较要健壮,后者可以将(例如)从一个Go版本更改为另一个Go版本。
迈克尔,

+ Michael Aug,如果有特定类型可用,那可能是更好的方法。
joonas.fi,

3

在您的情况下,您可以执行以下操作:

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
        // ....
    }
}

作为通用的应急路由器功能,这也将起作用:

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

3

简洁方式

对我而言,以下解决方案易于阅读,并向您展示了受测代码的自然代码流。

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

对于更通用的解决方案,您也可以这样:

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

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

0

您可以通过输入紧急信息来测试哪个功能发生了紧急情况

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.