Use assert when you want tests to continue after a failure:
import (
"testing""github.com/go-openapi/testify/v2/assert")
funcTestUser(t*testing.T) {
user:=GetUser(123)
// All three checks run, even if the first failsassert.NotNil(t, user)
assert.Equal(t, "Alice", user.Name)
assert.Equal(t, 25, user.Age)
}
Use require when subsequent checks depend on earlier ones:
import (
"testing""github.com/go-openapi/testify/v2/assert""github.com/go-openapi/testify/v2/require")
funcTestUser(t*testing.T) {
user:=GetUser(123)
// Stop immediately if user is nil (prevents panic on next line)require.NotNil(t, user)
// Only runs if user is not nilassert.Equal(t, "Alice", user.Name)
}
Rule of thumb: Use require for preconditions, assert for actual checks.
import (
"testing""github.com/go-openapi/testify/v2/assert""github.com/go-openapi/testify/v2/require")
funcTestFormatted(t*testing.T) {
// Add custom failure message with formattingassert.Equalf(t, 42, result, "expected answer to be %d", 42)
require.NotNilf(t, user, "user %d should exist", userID)
}
3. Forward Methods (Cleaner Syntax)
import (
"testing""github.com/go-openapi/testify/v2/assert""github.com/go-openapi/testify/v2/require")
funcTestForward(t*testing.T) {
a:=assert.New(t)
r:=require.New(t)
// No need to pass 't' each timea.Equal(42, result)
a.NotEmpty(list)
r.NotNil(user)
r.NoError(err)
}
import (
"testing""github.com/go-openapi/testify/v2/assert")
funcTestPanics(t*testing.T) {
// Function should panicassert.Panics(t, func() {
Divide(10, 0)
})
// Function should NOT panicassert.NotPanics(t, func() {
Divide(10, 2)
})
// Function should panic with specific valueassert.PanicsWithValue(t, "division by zero", func() {
Divide(10, 0)
})
}
Testify provides three assertions for testing asynchronous code: Eventually, Never, and EventuallyWith.
Warning
Asynchronous testing may sometimes be unavoidable. It should be avoided whenever possible.
Async tests (with timeouts, ticks etc) may easily become flaky under heavy concurrence on small CI runners.
When you’ve control over the code you test, always prefer sync tests, possibly with well-designed mocks.
Eventually: Wait for a Condition to Become True
Use Eventually when testing code that updates state asynchronously (background goroutines, event loops, caches).
import (
"sync""testing""time""github.com/go-openapi/testify/v2/assert")
funcTestBackgroundProcessor(t*testing.T) {
// Simulate a background processor that updates statevarprocessedboolvarmusync.Mutexgofunc() {
time.Sleep(50*time.Millisecond)
mu.Lock()
processed = truemu.Unlock()
}()
// Wait up to 200ms for the background task to complete,// checking every 10msassert.Eventually(t, func() bool {
mu.Lock()
defermu.Unlock()
returnprocessed }, 200*time.Millisecond, 10*time.Millisecond,
"background processor should have completed")
}
// Real-world example: Testing cache warmingfuncTestCacheWarming(t*testing.T) {
cache:=NewCache()
cache.StartWarmup() // Populates cache in background// Verify cache becomes ready within 5 secondsassert.Eventually(t, func() bool {
returncache.IsReady() &&cache.Size() > 0 }, 5*time.Second, 100*time.Millisecond,
"cache should warm up and contain entries")
}
Never: Ensure a Condition Remains False
Use Never to verify that something undesirable never happens during a time window (no data corruption, no invalid state).
import (
"sync/atomic""testing""time""github.com/go-openapi/testify/v2/assert")
funcTestNoDataCorruption(t*testing.T) {
varcounteratomic.Int32stopChan:= make(chanstruct{})
defer close(stopChan)
// Start multiple goroutines incrementing safelyfori:=0; i < 10; i++ {
gofunc() {
ticker:=time.NewTicker(5*time.Millisecond)
deferticker.Stop()
for {
select {
case<-stopChan:
returncase<-ticker.C:
counter.Add(1)
}
}
}()
}
// Verify counter never goes negative (indicating corruption)assert.Never(t, func() bool {
returncounter.Load() < 0 }, 500*time.Millisecond, 20*time.Millisecond,
"counter should never go negative")
}
// Real-world example: Testing rate limiter doesn't exceed thresholdfuncTestRateLimiter(t*testing.T) {
limiter:=NewRateLimiter(100) // 100 requests per second maxstopChan:= make(chanstruct{})
defer close(stopChan)
// Hammer the limiter with requestsfori:=0; i < 50; i++ {
gofunc() {
ticker:=time.NewTicker(1*time.Millisecond)
deferticker.Stop()
for {
select {
case<-stopChan:
returncase<-ticker.C:
limiter.Allow()
}
}
}()
}
// Verify we never exceed the rate limit over 2 secondsassert.Never(t, func() bool {
returnlimiter.CurrentRate() > 120// 20% tolerance }, 2*time.Second, 50*time.Millisecond,
"rate limiter should never exceed threshold")
}
EventuallyWith: Complex Async Assertions
Use EventuallyWith when you need multiple assertions to pass together.
The CollectT parameter lets you make regular assertions.
import (
"testing""time""github.com/go-openapi/testify/v2/assert")
funcTestAPIEventualConsistency(t*testing.T) {
// Simulate an eventually-consistent APIapi:=NewEventuallyConsistentAPI()
api.CreateUser("alice", "[email protected]")
// Wait for the user to be fully replicated across all shards// All conditions must pass in the same tickassert.EventuallyWith(t, func(c*assert.CollectT) {
user, err:=api.GetUser("alice")
// All these assertions must pass togetherassert.NoError(c, err, "user should be retrievable")
assert.NotNil(c, user, "user should exist")
assert.EqualT(c, "[email protected]", user.Email, "email should match")
assert.True(c, user.Replicated, "user should be replicated")
assert.GreaterOrEqual(c, user.ReplicaCount, 3, "should be on at least 3 replicas")
}, 10*time.Second, 500*time.Millisecond,
"user should be eventually consistent across all replicas")
}
// Real-world example: Testing distributed cache syncfuncTestDistributedCacheSync(t*testing.T) {
primary:=NewCacheNode("primary")
replica1:=NewCacheNode("replica1")
replica2:=NewCacheNode("replica2")
// Connect nodes for replicationprimary.AddReplica(replica1)
primary.AddReplica(replica2)
// Write to primaryprimary.Set("key", "value", 5*time.Minute)
// Verify value propagates to all replicas with correct TTLassert.EventuallyWith(t, func(c*assert.CollectT) {
val1, ttl1, ok1:=replica1.Get("key")
val2, ttl2, ok2:=replica2.Get("key")
// All replicas must have the valueassert.True(c, ok1, "replica1 should have the key")
assert.True(c, ok2, "replica2 should have the key")
// Values must matchassert.EqualT(c, "value", val1, "replica1 value should match")
assert.EqualT(c, "value", val2, "replica2 value should match")
// TTL should be approximately the same (within 1 second)assert.InDelta(c, 5*time.Minute, ttl1, float64(time.Second),
"replica1 TTL should be close to original")
assert.InDelta(c, 5*time.Minute, ttl2, float64(time.Second),
"replica2 TTL should be close to original")
}, 5*time.Second, 100*time.Millisecond,
"cache value should replicate to all nodes with correct TTL")
}
// Advanced: Using require in EventuallyWith to fail fastfuncTestEventuallyWithRequire(t*testing.T) {
api:=NewAPI()
assert.EventuallyWith(t, func(c*assert.CollectT) {
resp, err:=api.HealthCheck()
// Use require to stop checking this tick if request fails// This prevents nil pointer panics on subsequent assertionsassert.NoError(c, err, "health check should not error")
iferr!=nil {
return// Skip remaining checks this tick }
// Now safe to check response fieldsassert.EqualT(c, "healthy", resp.Status)
assert.Greater(c, resp.Uptime, 0)
assert.NotEmpty(c, resp.Version)
}, 30*time.Second, 1*time.Second,
"API should become healthy")
}
Key differences:
Eventually: Simple boolean condition, use for single checks
Never: Opposite of Eventually, verifies condition stays false
EventuallyWith: Complex checks with multiple assertions, use when you need detailed failure messages
Best practices:
Choose appropriate timeouts: long enough for async operations, short enough for fast test feedback
Choose appropriate tick intervals: frequent enough to catch state changes, infrequent enough to avoid overhead
Use EventuallyWith when you need to understand which assertion failed
Use Eventually for simple boolean conditions (faster, simpler)
Use Never to verify invariants over time (no race conditions, no invalid state)
Goroutine Leak Detection
Use NoGoRoutineLeak to verify that your code doesn’t leak goroutines. This is critical for long-running applications, connection pools, and worker patterns.
import (
"testing""github.com/go-openapi/testify/v2/assert")
funcTestWorkerPool(t*testing.T) {
assert.NoGoRoutineLeak(t, func() {
pool:=NewWorkerPool(10)
pool.Start()
// Submit workpool.Submit(func() { /* do something */ })
// Cleanup MUST happen inside the tested functionpool.Shutdown()
})
}
Why Use It?
Traditional goroutine leak detection (like go.uber.org/goleak) requires maintaining filter lists to exclude known system goroutines. This approach is brittle and prone to false positives when:
Running parallel tests
Using connection pools (database, HTTP, gRPC)
Background runtime goroutines change between Go versions
NoGoRoutineLeak uses pprof labels instead of stack-trace heuristics:
Only goroutines spawned by your tested function are checked
Pre-existing goroutines (runtime, connection pools, other tests) are ignored automatically
No configuration or filter lists needed
Works safely with t.Parallel()
Real-World Example: Testing a Server
import (
"net/http""testing""github.com/go-openapi/testify/v2/assert")
funcTestHTTPServer(t*testing.T) {
assert.NoGoRoutineLeak(t, func() {
// Start serverserver:=&http.Server{Addr: ":0", Handler: myHandler}
goserver.ListenAndServe()
// Do some requests...resp, _:=http.Get("http://"+server.Addr+"/health")
resp.Body.Close()
// Shutdown MUST happen inside the tested functionserver.Shutdown(context.Background())
})
}
Important: Cleanup Inside the Tested Function
Resource cleanup must happen inside the tested function, not via t.Cleanup():
// ❌ WRONG: t.Cleanup runs AFTER the leak checkfuncTestWrong(t*testing.T) {
varserver*Servert.Cleanup(func() { server.Stop() }) // Too late!assert.NoGoRoutineLeak(t, func() {
server = StartServer()
// Leak detected because server is still running })
}
// ✅ CORRECT: cleanup inside tested functionfuncTestCorrect(t*testing.T) {
assert.NoGoRoutineLeak(t, func() {
server:=StartServer()
deferserver.Stop() // Cleanup happens before leak check// ... test code ... })
}
Edge Cases
Panics: If the tested function panics while goroutines are still running, the leak is detected and reported along with the panic
t.FailNow()/runtime.Goexit(): Leaks are still detected even if the tested function exits early
Transitive goroutines: Goroutines spawned by child goroutines inherit the label and are tracked
Extensible assertions
The Assertions type may be extended to fit your needs like so.
import (
"fmt""strings""github.com/go-openapi/testify/v2/assert" )
// Assertions is a customized version of [assert.Assertions].typeAssertionsstruct {
*assert.Assertions }
funcNew(tassert.T) *Assertions {
return&Assertions{
Assertions: assert.New(t),
}
}
// StartsWith asserts that the string starts with the given prefix.//
// Examples://
// success: "hello world", "hello"// failure: "hello world", "bye"func (a*Assertions) StartsWith(str, prefixstring, msgAndArgs...any) bool {
ifh, ok:=a.T.(assert.H); ok {
h.Helper() // preserve the original failing location }
if !strings.HasPrefix(str, prefix) {
returna.Fail(fmt.Sprintf("Expected %q to start with %q", str, prefix), msgAndArgs...)
}
returntrue }
Note: Without the enable/yaml import, YAML assertions will panic with a helpful message.
Colorized Output (Optional)
Testify can colorize test failure output for better readability. This is an opt-in feature.
Enabling Colors
import (
"testing"_"github.com/go-openapi/testify/enable/colors/v2"// Enable colorized output"github.com/go-openapi/testify/v2/assert")
funcTestExample(t*testing.T) {
assert.Equal(t, "expected", "actual") // Failure will be colorized}
Activation
Colors are activated via command line flag or environment variable:
# Via flaggo test -v -testify.colorized ./...
# Via environment variableTESTIFY_COLORIZED=true go test -v ./...
Themes
Two themes are available for different terminal backgrounds:
# Dark theme (default) - bright colors for dark terminalsgo test -v -testify.colorized ./...
# Light theme - normal colors for light terminalsgo test -v -testify.colorized -testify.theme=light ./...
# Or via environmentTESTIFY_COLORIZED=true TESTIFY_THEME=light go test -v ./...
CI Environments
By default, colorization is disabled when output is not a terminal. To force colors in CI environments that support ANSI codes:
TESTIFY_COLORIZED=true TESTIFY_COLORIZED_NOTTY=true go test -v ./...
What Gets Colorized
Expected values in assertion failures (green)
Actual values in assertion failures (red)
Diff output:
Deleted lines (red)
Inserted lines (yellow)
Context lines (green)
Note: Without the enable/colors import, output remains uncolored (no panic, just no colors).