如何测试恐慌?

我目前正在考虑如何编写测试来检查给定的代码是否出现了混乱?我知道 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恐慌和我们恢复,或者如果功能没有恐慌。如果没有恐慌,如何指定跳过哪些代码,如果有恐慌,则执行哪些代码?我怎么才能知道我们是否从恐慌中恢复过来了呢?

58110 次浏览

您可以通过提供 panesan 输入来测试哪个函数感到恐慌

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

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相当弱。您可能对像 银杏这样更强大的测试引擎感兴趣。即使您不想要完整的银杏系统,您也可以只使用它的匹配器库 戈麦加,它可以与 testing一起使用。戈麦加包括了这样的配对者:

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

当您需要检查惊慌的内容时,您可以对恢复的值进行类型转换:

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

如果您正在测试的代码没有出现错误或惊慌,或者没有出现您希望它出现的错误消息,那么测试将会失败(这正是您所希望的)。

如果你使用 作证[断言],那么它就是一行程序:

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

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

当循环遍历多个测试用例时,我会使用下面这样的代码:

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

去游乐场

在你的情况下,你可以做:

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

惯用标准库解决方案

对我来说,下面的解决方案很容易阅读,并向维护人员展示了测试中的自然代码流。而且,它不需要第三方软件包。

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()) {
t.Helper()
defer func() { _ = recover() }()
f()
t.Errorf("should have panicked")
}

PS: _ = recover()用于满足代码不检查从 recover调用返回的错误的嘈杂的行程ーー这在本例中是完全可以接受的。

在 Go 中使用第三方软件包进行测试剥夺了 Go 测试的表现力。这就像使用函数不使用 if err != nil一样。

下面是我的恐慌预期

func TestPanic(t *testing.T) {


panicF := func() {
//panic call here
}
require.Panics(t, panicF)
}

我愿意

  1. testPanic1很简单
  2. 我更喜欢使用这种方式,因为它不足以预期会发生错误。它应该正是错误所在。
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
}

更多的例子

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

对标准库中的 strings_test.go进行了改编,生成了下面的解决方案。

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