stretchr/testify

Improve the behavior of `require` assertions when using `assert.EventuallyWithT`

Open

#1,396 opened on 2023年6月2日

GitHub で見る
 (12 comments) (8 reactions) (0 assignees)Go (25,958 stars) (1,704 forks)batch import
assert.Eventuallyhelp wantedpkg-assert

説明

Consider the following use of EventuallyWithT.

assert.EventuallyWithT(t, func(c *assert.CollectT) {
        exist, err := myFunc()
        require.NoError(c, err)
        assert.True(c, exist)
}, 1*time.Second, 10*time.Millisecond, "Object was not created")

Here the intention is to fail assert.EventuallyWithT immediately if the require.NoError assertions fails. Instead this code will panic if myFunc() returns an error. This is because of how the FailNow method is implemented on the CollectT object: https://github.com/stretchr/testify/blob/f97607b89807936ac4ff96748d766cf4b9711f78/assert/assertions.go#L1872-L1875

The panic ensures that the call to the condition function "exits" immediately. However it will immediately interrupt test execution. It feels like the implementation could be improved, so that it immediately fails the assert.EventuallyWithT assertion, without crashing the test.

There are 2 possible implementations IMO:

  1. change the FailNow implementation as follows:
func (c *CollectT) FailNow() {
        // a new channel in CollectT, to indicate that execution of EventuallyWithT should stop immediately
        c.failed <- struct{}
        // terminate the Goroutine evaluating the condition
	runtime.Goexit()
}

The relevant code in EventuallyWithT could then look like this:

	for tick := ticker.C; ; {
		select {
		case <-timer.C:
			collect.Copy(t)
			return Fail(t, "Condition never satisfied", msgAndArgs...)
		case <-tick:
			tick = nil
			collect.Reset()
			go func() { // this Goroutine will exit if FailNow is called
				condition(collect)
				ch <- len(collect.errors) == 0
			}()
		case v := <-ch:
			if v {
				return true
			}
			tick = ticker.C
		}
		case v := <-collect.failed: // new case
			collect.Copy(t)
			return Fail(t, "Require assertion failed", msgAndArgs...)
		}
	}
  1. the other option is to use recover in EventuallyWithT to recover from the panic. I am not going into too many details here, as I think the other solution is better. Panics can be cause by user code in the condition function, and we probably don't want to recover from these.

If the new suggested behavior is not acceptable, I'd like to understand the rationale for the current implementation.

cc @tnqn

コントリビューターガイド