Skip to content

Instantly share code, notes, and snippets.

@maratori
Last active October 27, 2022 22:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save maratori/010bfbf05639aa3a5ba832cdd75320ec to your computer and use it in GitHub Desktop.
Save maratori/010bfbf05639aa3a5ba832cdd75320ec to your computer and use it in GitHub Desktop.
func WaitFor() the best alternative for assert.Eventually()
package testhelpers
import (
"sync"
"time"
)
// WaitFor calls fn (once in tick) until it passes or timeout expired.
//
// Attempt is failed if any of following methods called in fn:
// - [TestingT].Fail()
// - [TestingT].FailNow()
// - [TestingT].Error()
// - [TestingT].Errorf()
// - [TestingT].Fatal()
// - [TestingT].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: [TestingT].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:
}
}
}
type TestingT interface {
Error(args ...interface{})
Errorf(format string, args ...interface{})
Fail()
FailNow()
Failed() bool
Fatal(args ...interface{})
Fatalf(format string, args ...interface{})
Log(args ...interface{})
Logf(format string, args ...interface{})
Name() string
Helper()
}
type fakeT struct {
sync.Mutex
failed bool
name string
}
func (t *fakeT) fail() {
t.Lock()
defer t.Unlock()
t.failed = true
}
func (t *fakeT) panic() {
t.fail()
panic("panic")
}
func (t *fakeT) Name() string {
t.Lock()
defer t.Unlock()
return t.name
}
func (t *fakeT) Failed() bool {
t.Lock()
defer t.Unlock()
return t.failed
}
func (t *fakeT) Error(args ...interface{}) { t.fail() }
func (t *fakeT) Errorf(format string, args ...interface{}) { t.fail() }
func (t *fakeT) Fail() { t.fail() }
func (t *fakeT) FailNow() { t.panic() }
func (t *fakeT) Fatal(args ...interface{}) { t.panic() }
func (t *fakeT) Fatalf(format string, args ...interface{}) { t.panic() }
func (t *fakeT) Log(args ...interface{}) {}
func (t *fakeT) Logf(format string, args ...interface{}) {}
func (t *fakeT) Helper() {}
package example
import (
"testing"
"time"
"x/testhelpers"
"github.com/stretchr/testify/assert"
)
func TestPassing(t *testing.T) {
ch := make(chan struct{})
go func() {
time.Sleep(5 * time.Second)
ch <- struct{}{}
}()
testhelpers.WaitFor(t, 10*time.Second, 1*time.Second, func(t testhelpers.TestingT) {
select {
case <-ch:
default:
assert.Fail(t, "channel is empty")
}
}) // PASS - output is empty
}
func TestFailing(t *testing.T) {
var ch chan struct{}
testhelpers.WaitFor(t, 10*time.Second, 1*time.Second, func(t testhelpers.TestingT) {
select {
case <-ch:
default:
assert.Fail(t, "channel is empty")
}
}) // FAIL
// Output:
// TestFailing: x_example_test.go:31:
// Error Trace: x_example_test.go:31
// wait_for.go:47
// x_example_test.go:27
// Error: channel is empty
// Test: TestFailing
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment