📖 9 min read (~ 1900 words).

Generics

Using Generic Assertions

Testify v2 provides 38 generic assertion functions that offer compile-time type safety alongside the traditional reflection-based assertions. Generic variants are identified by the T suffix (e.g., EqualT, GreaterT, ElementsMatchT).

Type Safety First

Generic assertions catch type mismatches when writing tests, not when running them. The performance improvements (1.2x-81x faster) are a bonus on top of this primary benefit.

Quick Start

Generic assertions work exactly like their reflection-based counterparts, but with compile-time type checking:

  • Reflection-based
    import "github.com/go-openapi/testify/v2/assert"
    
    func TestUser(t *testing.T) {
        expected := 42
        actual := getUserAge()
    
        // Compiles, but type errors appear at runtime
        assert.Equal(t, expected, actual)
    }
  • Generic (Type-safe)
    import "github.com/go-openapi/testify/v2/assert"
    
    func TestUser(t *testing.T) {
        expected := 42
        actual := getUserAge()
    
        // Compiler checks types immediately
        assert.EqualT(t, expected, actual)
    }

When to Use Generic Variants

✅ Use Generic Variants (*T functions) When:

  1. Testing with known concrete types - The most common case

    assert.EqualT(t, 42, result)              // int comparison
    assert.GreaterT(t, count, 0)              // numeric comparison
    assert.ElementsMatchT(t, expected, actual) // slice comparison
  2. You want refactoring safety - Compiler catches broken tests immediately

    // If getUserIDs() changes from []int to []string,
    // the compiler flags this line immediately
    assert.ElementsMatchT(t, expectedIDs, getUserIDs())
  3. IDE assistance matters - Autocomplete suggests only correctly-typed variables

    // Typing: assert.EqualT(t, expectedUser, actual
    //                                              ^
    // IDE suggests: actualUser ✓  (correct type)
    //               actualOrder ✗ (wrong type - grayed out)
  4. Performance-critical tests - See benchmarks for 1.2-81x speedups

🔄 Use Reflection Variants (no suffix) When:

  1. Intentionally comparing different types - Especially with EqualValues

    // Comparing int and int64 for semantic equality
    assert.EqualValues(t, int64(42), int32(42))  // ✓ Reflection handles this
    assert.EqualT(t, int64(42), int32(42))       // ❌ Compiler error
  2. Working with heterogeneous collections - []any or interface{} slices

    mixed := []any{1, "string", true}
    assert.Contains(t, mixed, "string")  // ✓ Reflection works
  3. Dynamic type scenarios - Where compile-time type is unknown

    var result interface{} = getResult()
    assert.Equal(t, expected, result)  // ✓ Reflection handles dynamic types
  4. Backward compatibility - Existing test code using reflection-based assertions

Type Safety Benefits

Catching Refactoring Errors

Generic assertions act as a safety net during refactoring:

  • Without Generics ❌
    // Original code
    type UserID int
    var userIDs []UserID
    
    assert.ElementsMatch(t, userIDs, getActiveUsers())
    
    // Later: UserID changes to string
    type UserID string
    var userIDs []UserID
    
    // Test still compiles!
    // Fails mysteriously at runtime or passes with wrong comparison
    assert.ElementsMatch(t, userIDs, getActiveUsers())
  • With Generics ✅
    // Original code
    type UserID int
    var userIDs []UserID
    
    assert.ElementsMatchT(t, userIDs, getActiveUsers())
    
    // Later: UserID changes to string
    type UserID string
    var userIDs []UserID
    
    // Compiler immediately flags the error
    assert.ElementsMatchT(t, userIDs, getActiveUsers())
    // ❌ Compile error: type mismatch!

Preventing Wrong Comparisons

Generic assertions force you to think about what you’re comparing:

  • Pointer vs Value Comparison
    expected := &User{ID: 1, Name: "Alice"}
    actual := &User{ID: 1, Name: "Alice"}
    
    // Reflection: Compares pointer addresses (probably wrong)
    assert.Equal(t, expected, actual)  // ✗ Fails (different addresses)
    
    // Generic: Makes the intent explicit
    assert.EqualT(t, expected, actual)   // Compares pointers
    assert.EqualT(t, *expected, *actual) // Compares values ✓
  • Type Confusion Prevention
    userID := 42
    orderID := "ORD-123"
    
    // Reflection: Compiles, wrong comparison
    assert.Equal(t, userID, orderID)  // Runtime failure
    
    // Generic: Compiler catches the mistake
    assert.EqualT(t, userID, orderID)  // ❌ Compile error!

Available Generic Functions

Testify v2 provides generic variants across all major domains:

Equality (4 functions)

  • EqualT[V comparable] - Type-safe equality for comparable types
  • NotEqualT[V comparable] - Type-safe inequality
  • SameT[V comparable] - Pointer identity check
  • NotSameT[V comparable] - Different pointer check

Comparison (6 functions)

  • GreaterT[V Ordered] - Type-safe greater-than comparison
  • GreaterOrEqualT[V Ordered] - Type-safe >=
  • LessT[V Ordered] - Type-safe less-than comparison
  • LessOrEqualT[V Ordered] - Type-safe <=
  • PositiveT[V SignedNumeric] - Assert value > 0
  • NegativeT[V SignedNumeric] - Assert value < 0

Collection (12 functions)

  • StringContainsT[S Text] - String/byte slice contains substring
  • SliceContainsT[E comparable] - Slice contains element
  • MapContainsT[K comparable, V any] - Map contains key
  • SeqContainsT[E comparable] - Iterator contains element (Go 1.23+)
  • ElementsMatchT[E comparable] - Slices have same elements (any order)
  • SliceSubsetT[E comparable] - Slice is subset of another
  • Plus negative variants: *NotContainsT, NotElementsMatchT, SliceNotSubsetT

Ordering (6 functions)

  • IsIncreasingT[E Ordered] - Slice elements strictly increasing
  • IsDecreasingT[E Ordered] - Slice elements strictly decreasing
  • IsNonIncreasingT[E Ordered] - Slice elements non-increasing (allows equal)
  • IsNonDecreasingT[E Ordered] - Slice elements non-decreasing (allows equal)
  • SortedT[E Ordered] - Slice is sorted (generic-only function)
  • NotSortedT[E Ordered] - Slice is not sorted (generic-only function)

Numeric (2 functions)

  • InDeltaT[V Measurable] - Numeric comparison with absolute delta (supports integers and floats)
  • InEpsilonT[V Measurable] - Numeric comparison with relative epsilon (supports integers and floats)

Boolean (2 functions)

  • TrueT[B Boolean] - Assert boolean is true
  • FalseT[B Boolean] - Assert boolean is false

String (2 functions)

  • RegexpT[S Text] - String matches regex (string or []byte)
  • NotRegexpT[S Text] - String doesn’t match regex

Type (2 functions)

  • IsOfTypeT[EType any] - Assert value is of type EType (no dummy value needed!)
  • IsNotOfTypeT[EType any] - Assert value is not of type EType

JSON & YAML (2 functions)

  • JSONEqT[S Text] - JSON strings are semantically equal
  • YAMLEqT[S Text] - YAML strings are semantically equal
See Complete API

For detailed documentation of all generic functions, see the API Reference organized by domain.

Practical Examples

Example 1: Collection Testing

  • Type-Safe Collection Assertions
    func TestUserPermissions(t *testing.T) {
        user := getUser(123)
    
        expectedPerms := []string{"read", "write"}
        actualPerms := user.Permissions
    
        // Compiler ensures both slices are []string
        assert.ElementsMatchT(t, expectedPerms, actualPerms)
    
        // Check subset relationship
        assert.SliceSubsetT(t, []string{"read"}, actualPerms)
    }
  • Iterator Support (Go 1.23+)
    func TestSequenceContains(t *testing.T) {
        // iter.Seq[int] from Go 1.23
        numbers := slices.Values([]int{1, 2, 3, 4, 5})
    
        // Type-safe iterator checking
        assert.SeqContainsT(t, numbers, 3)
        assert.SeqNotContainsT(t, numbers, 99)
    }

Example 2: Numeric Comparisons

  • Ordered Types
    func TestPricing(t *testing.T) {
        price := calculatePrice(item)
        discount := calculateDiscount(item)
    
        // Type-safe numeric comparisons
        assert.PositiveT(t, price)
        assert.GreaterT(t, price, discount)
        assert.LessOrEqualT(t, discount, price)
    }
  • Float Comparisons
    func TestPhysicsCalculation(t *testing.T) {
        result := calculateVelocity(mass, force)
        expected := 42.0
    
        // Type-safe float comparison with delta
        assert.InDeltaT(t, expected, result, 1e-6)
    
        // Or with epsilon (relative error)
        assert.InEpsilonT(t, expected, result, 0.001)
    }

Example 3: Type Checking Without Dummy Values

The IsOfTypeT function eliminates the need for dummy values:

  • Old Way (Reflection)
    func TestGetUser(t *testing.T) {
        result := getUser(123)
    
        // Need to create a dummy User instance
        assert.IsType(t, User{}, result)
    
        // Or use a pointer dummy
        assert.IsType(t, (*User)(nil), result)
    }
  • New Way (Generic)
    func TestGetUser(t *testing.T) {
        result := getUser(123)
    
        // No dummy value needed!
        assert.IsOfTypeT[User](t, result)
    
        // For pointer types
        assert.IsOfTypeT[*User](t, result)
    }

Example 4: Sorting and Ordering

  • Ordering Checks
    func TestSortedData(t *testing.T) {
        timestamps := []int64{
            1640000000,
            1640000100,
            1640000200,
        }
    
        // Type-safe ordering assertions
        assert.IsIncreasingT(t, timestamps)
        assert.SortedT(t, timestamps)  // Generic-only function
    }
  • Custom Ordered Types
    type Priority int
    
    const (
        Low Priority = iota
        Medium
        High
    )
    
    func TestPriorities(t *testing.T) {
        tasks := []Priority{Low, Medium, High}
    
        // Works with Ordered types (custom types supported)
        assert.IsNonDecreasingT(t, tasks)
    }

Migration Guide

Step 1: Identify High-Value Targets

Start with the most common assertions that benefit most from type safety:

// High value: Collection operations (also get big performance wins)
assert.Equal  assert.EqualT
assert.ElementsMatch  assert.ElementsMatchT
assert.Contains  assert.ContainsT (SliceContainsT/MapContainsT/StringContainsT)

// High value: Comparisons (eliminate allocations)
assert.Greater  assert.GreaterT
assert.Less  assert.LessT
assert.Positive  assert.PositiveT

// High value: Type checks (cleaner API)
assert.IsType(t, User{}, v)  assert.IsOfTypeT[User](t, v)

Step 2: Automated Search & Replace

Use your IDE or tools to find and replace systematically:

# Find all Equal assertions
grep -r "assert\.Equal(" . --include="*_test.go"

# Find all require.Greater assertions
grep -r "require\.Greater(" . --include="*_test.go"

Step 3: Fix Compiler Errors

The compiler will catch type mismatches. This is a feature, not a bug:

  • Compiler Error
    // Original code
    assert.EqualT(t, int64(result), count)
    // ❌ Error: mismatched types int64 and int
  • Fix Option 1: Same Type
    // Convert to same type
    assert.EqualT(t, int64(result), int64(count))
  • Fix Option 2: Use Reflection
    // If cross-type comparison is intentional
    assert.Equal(t, int64(result), count)

Step 4: Incremental Adoption

You don’t need to migrate everything at once:

func TestMixedAssertions(t *testing.T) {
    // Use generic where types are known
    assert.EqualT(t, 42, getAge())
    assert.GreaterT(t, count, 0)

    // Keep reflection for dynamic types
    var result interface{} = getResult()
    assert.Equal(t, expected, result)

    // Both styles coexist peacefully
}

Performance Benefits

Generic assertions provide significant performance improvements, especially for collection operations:

OperationSpeedupWhen It Matters
ElementsMatchT21-81x fasterLarge collections, hot test paths
EqualT10-13x fasterMost common assertion
GreaterT/LessT10-22x fasterNumeric comparisons
SliceContainsT16x fasterCollection membership tests
Learn More

See the complete Performance Benchmarks for detailed analysis and real benchmark results.

Best Practices

✅ Do

  1. Prefer generic variants by default - Type safety is always valuable

    assert.EqualT(t, expected, actual)  // ✓ Type safe
  2. Let the compiler guide you - Type errors reveal design issues

    // Compiler error reveals you're comparing wrong types
    assert.EqualT(t, userID, orderID)  // ❌ Good - catches mistake!
  3. Use explicit types for clarity

    assert.IsOfTypeT[*User](t, result)  // ✓ Clear intent
  4. Leverage performance wins in hot paths - Generic assertions are faster

    // Table-driven tests with many iterations
    for _, tc := range testCases {
        assert.EqualT(t, tc.expected, tc.actual)  // ✓ Fast
    }

❌ Don’t

  1. Don’t force generics for dynamic types

    var result interface{} = getResult()
    assert.Equal(t, expected, result)  // ✓ Reflection is fine here
  2. Don’t use reflection to avoid fixing types

    // Bad: Using reflection to bypass type safety
    assert.Equal(t, expected, actual)  // ✗ Defeats the purpose
    
    // Good: Fix the types or use EqualValues if intentional
    assert.EqualT(t, expected, actual)  // ✓ Type safe
  3. Don’t create unnecessary type conversions

    // Bad: Unnecessary conversion
    assert.EqualT(t, int64(42), int64(result))
    
    // Good: Work with natural types
    assert.EqualT(t, 42, result)

Type Constraints Reference

Generic assertions use custom type constraints defined in internal/assertions/generics.go:

ConstraintDefinitionDescriptionExample Types
comparableGo built-inTypes that support == and !=int, string, bool, pointers, structs (if all fields are comparable)
Boolean~boolBoolean and named bool typesbool, type MyBool bool
Text~string | ~[]byteString or byte slice typesstring, []byte, custom string/byte types
Orderedcmp.Ordered | []byte | time.TimeExtends cmp.Ordered with byte slices and timeStandard ordered types plus []byte and time.Time
SignedNumeric~int... | ~float32 | ~float64Signed integers and floatsint, int8-int64, float32, float64
UnsignedNumeric~uint...Unsigned integersuint, uint8-uint64
MeasurableSignedNumeric | UnsignedNumericAll numeric types (for delta comparisons)Used by InDeltaT/InEpsilonT - supports integers AND floats
RegExpText | *regexp.RegexpRegex pattern or compiled regexpstring, []byte, *regexp.Regexp
Key Differences from Standard Go Constraints
  • Ordered is extended: Adds []byte and time.Time to cmp.Ordered for seamless bytes.Compare() and time.Time.Compare() support
  • Measurable supports integers: InDeltaT and InEpsilonT work with both integers and floats, not just floating-point types
  • Custom type support: All constraints use the ~ operator to support custom types (e.g., type UserID int)

Summary

Generic assertions in testify v2 provide:

Type Safety: Catch errors when writing tests, not when running them ✅ Performance: 1.2x to 81x faster than reflection-based assertions ✅ Better IDE Support: Autocomplete suggests correctly-typed values ✅ Refactoring Safety: Compiler catches broken tests immediately ✅ Zero Downside: Always as fast or faster than reflection variants

Start using generic assertions today - add the T suffix to your existing assertions and let the compiler guide you to better, safer tests.


Quick Reference
  • Generic functions: Add T suffix (e.g., EqualT, GreaterT, ElementsMatchT)
  • Format variants: Add Tf suffix (e.g., EqualTf, GreaterTf)
  • When to use: Prefer generics for known concrete types
  • When not to: Keep reflection for dynamic types and cross-type comparisons
  • Performance: See benchmarks for dramatic speedups