The MustCreate Pattern for Go Tests

I recently discovered a handy way to make Go tests more readable and less repetitive.

Suppose I have a constructor that can fail:

func NewUsername(raw string) (Username, error) { ... }

And I have another function that takes a Username as an argument:

func (ds Datastore) ReadUser(username Username) (User, error) { ... }

When I write tests for ReadUser, I call NewUsername with a value I know is valid, but I still have to include a lot of verbose error-checking code to convince the compiler that everything is okay:

username, err := NewUsername("joe123")

// This is silly because *I* know joe123 is a valid username, so this should
// never happen.
if err != nil {
  t.Fatalf("failed to create username: %v", err)
}

// Perform the test assertion I actually care about.
user, err := store.ReadUser(username)
if got, want := user.Name.String(), "joe123"; got != want {
  t.Errorf("username=%v, want=%v", got, want)
}

My old solution: one new helper function per type ๐Ÿ”—︎

Until a few months ago, I avoided the verbose error checking by writing a helper function like MustCreateUsername that wraps NewUsername with the expectation that it never returns a non-nil error value:

user, err := store.ReadUser(MustCreateUsername(t, "joe123"))
if got, want := user.Name.String(), "joe123"; got != want {
  t.Errorf("username=%v, want=%v", got, want)
}

The implementation of MustCreateUsername looks how you’d expect:

func MustCreateUsername(t testing.TB, raw string) Username {
  t.Helper()
  username, err := NewUsername(raw)
  if err != nil {
    t.Fatalf("invalid username: %s", raw)
  }
}

The helper function strategy worked fine, but it scales poorly. Eventually, my repos become littered with MustCreateFoo and MustCreateBar for every type in my app.

New solution: A single, generic MustCreate helper ๐Ÿ”—︎

In 2022, Go 1.18 added support for generics. I like the idea of generics, but I rarely encounter problems where generics are the solution.

I recently realized that generics are the better way to solve my sprawling MustCreateX problem. Instead of defining a helper MustCreateX for every type, I defined a single, generic MustCreate test helper, so my test becomes:

user, err := store.ReadUser(testutil.MustCreate(t, NewUsername, "joe123"))
if got, want := user.Name.String(), "joe123"; got != want {
  t.Errorf("username=%v, want=%v", got, want)
}

The magic is the generic MustCreate function:

func MustCreate[RawInput, CreatedValue any](
  t testing.TB,
  create func(RawInput) (CreatedValue, error),
  rawInput RawInput,
) CreatedValue {
  t.Helper()
  createdValue, err := create(rawInput)
  if err != nil {
    t.Fatalf("failed to create test value: %v", err)
  }
  return createdValue
}

I have trouble reasoning about generics, so I’ll walk through the function implementation. I invoke MustCreate like this in my tests:

testutil.MustCreate(t, NewUsername, "joe123")

The above call is equivalent to this more explicit version:

testutil.MustCreate[string, Username](t, NewUsername, "joe123")

When I call MustCreate here, RawInput is string (the type of the raw username value), and CreatedValue is Username (the return type). It’s equivalent to an implementation that looks like this:

func MustCreate(
  t testing.TB,
  create func(string) (Username, error),
  rawInput string,
) CreatedValue {
  t.Helper()
  createdValue, err := create(rawInput)
  if err != nil {
    t.Fatalf("failed to create test value: %v", err)
  }
  return createdValue
}

Extending to multi-param constructors ๐Ÿ”—︎

What about constructors that have more than one parameter?

func NewRange(start int, end int) (Range, error) { ... }

In those cases, I write an additional helper called MustCreate2:

range := testutil.MustCreate2(t, models.NewRange, 0, 5)

The implementation of MustCreate2 looks just like MustCreate but with an additional type for the second parameter:

func MustCreate2[RawInput1, RawInput2, CreatedValue any](
  t testing.TB,
  create func(RawInput1, RawInput2) (CreatedValue, error),
  rawInput1 RawInput1,
  rawInput2 RawInput2,
) CreatedValue {
  t.Helper()
  createdValue, err := create(rawInput1, rawInput2)
  if err != nil {
    t.Fatalf("failed to create test value: %v", err)
  }
  return createdValue
}

Conclusion ๐Ÿ”—︎

The MustCreate pattern is nothing revolutionary, but it’s a technique that improved my Go tests and reduced some mindless toil from my dev work.

Read My Book

I wrote a book about simple techniques to help developers improve their writing.

My book will teach you how to:

Buy the book