测试从恐慌中恢复

huangapple go评论79阅读模式
英文:

Testing recover from panic

问题

我想测试一个构造函数,但是如果没有提供某些数据,我需要引发 panic,那么在测试中如何从 panic 中恢复呢?

目前,我在我的 TestNew 函数中添加了一个带有 recover 的 defer,但是如果我的 map 中的某个元素具有空的 URL,那么其余的元素将不会被检查。

t.go

package testing

type test {
  url string
}

func New(ops map[string]string) *test {
  if ops["url"] == "" {
    panic("Url missing")
  }
  var t = new(test)
  t.url = ops["url"]
  return t
}

t_test.go

package testing

type testTest map[string]string
var testingTest = []testTest {
  testTest {
    "url": "test",
  },
  testTest{
    "url": "",
  },
}

func NewTest(t *testing.T) {
  defer func() {
    recover()
  }()

  for _, e := range testingTest {
    url := New(e)
    url.hasUrl(t, e["url"])
  }
}

func (s *test) hasUrl(t *testing.T, u string) {
  if s.url != u {
    t.Errorf("Expected %s to be equal with %s", s.url, u)
  }
}
英文:

I want to test a constructor, but in case some data is not provided I need to panic, how can I recover from that panic in testing?

At the moment I've added in my TestNew func a defer with recover, but in case an element from my map has an empty URL the rest will not be checked.

t.go

package testing

type test {
  url string
}

func New(ops map[string]string) *test {
  if ops["url"] == "" {
    panic("Url missing")
  }
  var t = new(test)
  t.url = ops["url"]
  return t
}

t_test.go

package testing

type testTest map[string]string
var testingTest = []testTest {
  testTest {
    "url": "test",
  },
  testTest{
    "url": "",
  },
}

func NewTest(t *testing.T) {
  defer func() {
    recover()
  }()

  for _, e := range testingTest {
    url := New(e)
    url.hasUrl(t, e["url"])
  }
}

func (s *test) hasUrl(t *testing.T, u string) {
  if s.url != u {
    t.Errorf("Expected %s to be equal with %s", s.url, u)
  }
}

答案1

得分: 3

我认为,为依赖于 panic/recover 的库设计 API 不是正确的方式。Go 有错误模式,所以如果 New 方法无法进行测试,可以返回状态而不是 panic。

package testing

type test struct {
  url string
}

func New(ops map[string]string) (*test, bool) {
  if ops["url"] == "" {
    return nil, false
  }
  var t = new(test)
  t.url = ops["url"]
  return t, true
}

然后可以这样使用:

for _, e := range testingTest {
  url, ok := New(e)
  if ok {
    url.hasUrl(t, e["url"])
  }
}

如果你坚持使用 panic,那么可以将调用包装在函数中,并在其中进行 recover。但是你仍然需要向调用者提供状态。

package main

import "fmt"

func test(e int) {
    if e == 2 {
        panic("panic!")
    }
}

func main() {
    for _, e := range []int{1, 2, 3} {
        func() {
            defer func() { recover() }()
            test(e)
            fmt.Println("testing", e)
        }()
    }
}
英文:

I would say that designing API for library that relies on panic/recover is not a right way. Go has error pattern so if the method New cannot test, it can return the status instead.

package testing

type test {
  url string
}

func New(ops map[string]string) (*test, bool) {
  if ops["url"] == "" {
    return nil, false
  }
  var t = new(test)
  t.url = ops["url"]
  return t, true
}

and then

for _, e := range testingTest {
  url, ok := New(e)
  if ok {
    url.hasUrl(t, e["url"])
  }
}

If you insist on using panic, then you can wrap the invocation into function and recover in it. But then you still would need to provide the status to the caller.

package main

import "fmt"

func test(e int) {
    if e == 2 {
        panic("panic!")
    }
}

func main() {
    for _, e := range []int{1, 2, 3} {
        func() {
            defer func() { recover() }()
            test(e)
            fmt.Println("testing", e)
        }()
    }
}

答案2

得分: 2

你的实现存在一些小问题。首先,你无法确定是否已经恢复,因为你调用了方法但忽略了返回值。所以,首先让我们将这段代码转换为:

func NewTest(t *testing.T) {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println("Recovered in NewTest", r)
    }
  }()

  for _, e := range testingTest {
    url := New(e)
    url.hasUrl(t, e["url"])
  }
}

另外一个问题是,你错误地使用了defer。你在NewTest的顶部使用了defer,这意味着当它被调用时,你即将退出新的测试。相反,你应该将它放在发生panic的方法中。现在你已经将它放在现在的位置,恢复并继续迭代已经太迟了。当你恢复时,它是在调用NewTest的地方。所以应该这样做:

func (s *test) hasUrl(t *testing.T, u string) {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println("Recovered in NewTest", r)
    }
  }()
  if s.url != u {
    t.Errorf("Expected %s to be equal with %s", s.url, u)
  }
}

我创建了一个快速示例问题来演示这一点。将defer/recover移到NewTest中,你会发现它只打印一次;在当前形式下,它会打印10次,因为当我恢复时,我仍然在循环内部。这是示例代码

编辑:抱歉,我的示例有点误导人,因为我在将你的代码片段复制到playground时,将panic/recover部分移到了hasUrl中。在你的情况下,实际上应该是这样的:

func New(ops map[string]string) *test {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println("Recovered while panicking in New")
    }
  }()
  if ops["url"] == "" {
    panic("Url missing")
  }
  var t = new(test)
  t.url = ops["url"]
  return t
}

当然,这个示例相当牵强,没有太多意义。如果你明确地发生panic,我会说调用范围应该使用recover,如果你调用的是一个发生panic的库,那么你应该使用recover。

英文:

There are a few small problems with your implementation. For one, you can't even be sure you have recovered because you're calling the method but ignoring the return value. So for starters lets convert this;

func NewTest(t *testing.T) {
  defer func() {
    recover()
  }()

  for _, e := range testingTest {
    url := New(e)
    url.hasUrl(t, e["url"])
  }
}

to this;

func NewTest(t *testing.T) {
  defer func() {
    if r := recover(); r != nil {
            fmt.Println("Recovered in NewTest", r)
        }
  }()

  for _, e := range testingTest {
    url := New(e)
    url.hasUrl(t, e["url"])
  }
}

Now the other problem... You're using defer wrong. You defer at the top of NewTest meaning when it is called, you're about to exit new test. Instead you want it in the method that is panicing. It's too late to recover and continue iteration at the point you have it now. When you recover it's at the place where NewTest was called. So this should do it;

func (s *test) hasUrl(t *testing.T, u string) {
  defer func() {
        if r := recover(); r != nil {
                fmt.Println("Recovered in NewTest", r)
            }
      }()
  if s.url != u {
    t.Errorf("Expected %s to be equal with %s", s.url, u)
  }
}

I made a quick sample problem to demonstrate this. Move the defer/recover into NewTest and you will find it only prints once, in the current form it prints 10 times because when I recover, I'm still inside the loop. https://play.golang.org/p/ZA1Ijvsimz

EDIT: Sorry my example was a bit misleading because I moved the panic/recover bits into hasUrl when I was copying bits of your code into the playground. In your case it would actually be this;

func New(ops map[string]string) *test {
  defer func() {
       if r := recover(); r != nil {
             fmt.Println("Recovered while panicing in New")
       }
  }
  if ops["url"] == "" {
    panic("Url missing")
  }
  var t = new(test)
  t.url = ops["url"]
  return t
}

Of course that example is pretty contrived and doesn't make a whole log of sense. If you're explicitly panicing, I'd say the calling scope should be the one using recover, if you're calling a library which panics, then you should be the one using recover.

答案3

得分: 0

我对问题中的词语感到困惑,但我认为它与我有相同的关注点。您定义了一个在违反前提条件时引发 panic 的函数,而不是选择其他方式(返回错误、使用默认值、什么都不做...),就像对数组做出的合理选择一样:

var a [1]int
a[1] = 0 // 引发 panic

有时,您希望编写一个测试来证明前提条件的违反会引发 panic,而不是被忽略。之前的答案并不完整,所以我得到了以下代码:

func GreatBigFunction(url string) {
	if url == "" {
		panic("panic!")
	}
}

func main() {
	var tests = []struct {
		url       string
		completes bool
	}{
		{"a", true},
		{"", false},
		{"b", true},
		{"c", false}, // 错误的预期
		{"", true},   // 错误的预期
	}
	for _, test := range tests {
		fmt.Printf("testing that \"%s\" %s\n", test.url, map[bool]string{false: "panics", true: "completes"}[test.completes])
		func() {
			if !test.completes {
				defer func() {
					p := recover()
					if p == nil {
						fmt.Println("t.Fail: should have panicked")
					}
				}()
			}
			GreatBigFunction(test.url)
		}()
	}

	fmt.Println("Bye")
}

最后一个测试用例检查如果测试用例表示函数不应该引发 panic,那么正常的 panic 处理是否仍然有效。

相同的代码也可以在 Playground 上找到。还可以忽略 recover 的返回值,但仍然可靠地报告未捕获的 panic

英文:

I'm confused by the words in the question, but I think it started with the same concern I had. You have defined a function to panic when a precondition is violated, rather than an alternative (returning an error, using defaults, doing nothing...); just like the reasonable choice made for arrays:

var a [1]int
a[1] = 0 // panic

Sometimes you want to write a test that proves a precondition violation causes a panic and is not swept under the rug. The previous answers aren't quite complete, so this is what I got to:

func GreatBigFunction(url string) {
	if url == "" {
		panic("panic!")
	}
}

func main() {
	var tests = []struct {
		url       string
		completes bool
	}{
		{"a", true},
		{"", false},
		{"b", true},
		{"c", false}, // wrong expectation
		{"", true},   // wrong expectation
	}
	for _, test := range tests {
		fmt.Printf("testing that \"%s\" %s\n", test.url, map[bool]string{false: "panics", true: "completes"}[test.completes])
		func() {
			if !test.completes {
				defer func() {
					p := recover()
					if p == nil {
						fmt.Println("t.Fail: should have panicked")
					}
				}()
			}
			GreatBigFunction(test.url)
		}()
	}

	fmt.Println("Bye")
}

The last test case checks that the normal panic handling prevails if the test case says the function isn't supposed to panic.

Same code is on Playground. It's also possible to ignore the return value of recover but still reliably report a missed panic.

huangapple
  • 本文由 发表于 2015年7月30日 00:29:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/31706145.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定