testify icon indicating copy to clipboard operation
testify copied to clipboard

Better alternative to Eventually()

Open maratori opened this issue 4 years ago • 4 comments

I've found that Eventually is not really useful:

  1. I can't use assertions inside condition.
  2. Error message doesn't help to understand failure cause: Condition never satisfied.

So I use func WaitFor instead: gist.

The only disadvantage compared to Eventually:

  • WaitFor will not stop after timeout (immediately). It will call function one more time to pass error message from assertions inside that function to the test.
// WaitFor calls fn (once in tick) until it passes or timeout expired.
//
// Attempt is failed if any of following methods called in fn:
//  - Fail/FailNow
//  - Error/Errorf
//  - Fatal/Fatalf
// Otherwise attempt is considered as successful and WaitFor stops.
//
// WaitFor receives test object as the first argument.
// It is passed to fn only on the last attempt. All other attempts use fakeT.
// So the test will fail only after timeout expired.
//
// Note: t.Log() and other methods will print to stdout only on the last attempt.
func WaitFor(t TestingT, timeout time.Duration, tick time.Duration, fn func(t TestingT)) {
	timer := time.NewTimer(timeout)
	ticker := time.NewTicker(tick)
	defer timer.Stop()
	defer ticker.Stop()
	for {
		ft := &fakeT{name: t.Name()}
		didPanic := false
		func() {
			defer func() {
				if recover() != nil {
					didPanic = true
				}
			}()
			fn(ft)
		}()
		if !ft.Failed() && !didPanic {
			return
		}

		select {
		case <-timer.C:
			fn(t)
			return
		case <-ticker.C:
		}
	}
}

The rest code (with examples) can be found in this gist.

It would be great to see the same feature in testify.

maratori avatar Mar 01 '20 15:03 maratori

I'd also like to have this.

@maratori Do you happen to have put this in some other public package in the meantime where this would be ready for use now?

julianbrost avatar May 12 '21 11:05 julianbrost

@julianbrost It is used in several repos, but they are private.

maratori avatar May 12 '21 13:05 maratori

I'm currently running into this same need. In particular, I have lots of tests that take this form:

PerformAnAction()

data := GetDataForValidation()

//Contains *multiple* assertions of varying types
ValidateTheData(data)

Some of these tests require polling behavior. It would be nice to be able to recover from failed assertions so I can re-run the second and third lines on a timer. Currently, I have to resort to either panic and recover or classic error return values and bypass assert entirely.

I think the most flexible solution might be to change the signature of condition to func(Assertions) bool, so that Eventually can pass a special "soft" assertion object to the condition. Failures from the soft assertion object would cause a retry, and only failures on the final pass would get turned into "hard" failures.

l-abels avatar Mar 30 '22 21:03 l-abels

Actually, looking at the way this library tests its own asserts by giving it a dummy testing.T: assert := New(new(testing.T))

It shouldn't be too hard to do the same thing with a custom testing.T implementation, right? It could serve up a list of all of the inputs to its Errorf and allow us to replay them onto the real testing.T afterwards. I'll take a look at implementing that locally.

l-abels avatar Mar 30 '22 21:03 l-abels

This is now assert.EventuallyWithT.

dolmen avatar Jul 31 '23 13:07 dolmen