Table Testing Go Generics

Issue

I’m excited for Go 1.18 and wanted to test the new generics feature.
Feels pretty neat to use, but I stumbled over an issue:

How do you table test generic functions?

I came up with this code, but I need to redeclare my testing logic over each function since I can’t instantiate T values.
(Inside my project I use structs instead of string and int. Just didn’t want to include them because it’s already enough code)

How would you approach this problem?

Edit:
Here’s the code:

package main

import (
    "testing"

    "github.com/stretchr/testify/assert"
)

type Item interface {
    int | string
}

type store[T Item] map[int64]T

// add adds an Item to the map if the id of the Item isn't present already
func (s store[T]) add(key int64, val T) {
    _, exists := s[key]
    if exists {
        return
    }
    s[key] = val
}

func TestStore(t *testing.T) {
    t.Run("ints", testInt)
    t.Run("strings", testString)
}

type testCase[T Item] struct {
    name     string
    start    store[T]
    key      int64
    val      T
    expected store[T]
}

func testString(t *testing.T) {
    t.Parallel()
    tests := []testCase[string]{
        {
            name:  "empty map",
            start: store[string]{},
            key:   123,
            val:   "test",
            expected: store[string]{
                123: "test",
            },
        },
        {
            name: "existing key",
            start: store[string]{
                123: "test",
            },
            key: 123,
            val: "newVal",
            expected: store[string]{
                123: "test",
            },
        },
    }
    for _, tc := range tests {
        t.Run(tc.name, runTestCase(tc))
    }
}

func testInt(t *testing.T) {
    t.Parallel()
    tests := []testCase[int]{
        {
            name:  "empty map",
            start: store[int]{},
            key:   123,
            val:   456,
            expected: store[int]{
                123: 456,
            },
        },
        {
            name: "existing key",
            start: store[int]{
                123: 456,
            },
            key: 123,
            val: 999,
            expected: store[int]{
                123: 456,
            },
        },
    }
    for _, tc := range tests {
        t.Run(tc.name, runTestCase(tc))
    }
}

func runTestCase[T Item](tc testCase[T]) func(t *testing.T) {
    return func(t *testing.T) {
        tc.start.add(tc.key, tc.val)
        assert.Equal(t, tc.start, tc.expected)
    }
}

Solution

I need to redeclare my testing logic over each function

Correct.

Your function runTestCase[T Item](tc testCase[T]) already provides a reasonable level of abstraction. As you did, you can put there some common logic about starting the test and verifying the expected outcome. However that’s about it.

A generic type (or function) under test has to be instantiated with some concrete type sooner or later, and one single test table can only include either one of those types — or interface{}/any, which you can not use to satisfy a specific constraint like int | string.

However, you probably do not need to always test every possible type parameter. The purpose of generics is to write code that works with arbitrary types, and in particular the purpose of constraints is to write code with arbitrary types that support the same operations.

I’d advise to write unit tests for different types only if the code makes use of operators that have different meanings. For example:

  • the + operator for numbers (sum) and strings (concatenation)
  • the < and > for numbers (greater, lesser) and strings (lexicographically before or after)

See also this where the OP was attempting to do something similar

Answered By – blackgreen

Answer Checked By – David Marino (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.