Test Fixtures in Go
Tags: go, testing
Published: 2023-03-13 03:56:15

When testing, we often need some kind of test data. In Go, test data can often come in the form of structs. In this post, we explore various methods of creating reusable test data, and I will also introduce a library I am currently working on to assist you in the process of creating test data.

## Creating Test Data

Let’s assume we have a structure as follows:

```go
type Book struct{
   ID int
   Title string
}

type Author struct {
   ID int
   Name string
   Books []Book
}

func GetAuthor() Author {
// return an author
}

```

In most common IDEs, you can generate unit tests for a function. In most cases, a table-driven test is then generated, similar to the following:

```go
func TestGetAuthor(t *testing.T) {
	tests := []struct {
		name string
		want Author
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := GetAuthor(); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("GetAuthor() = %v, want %v", got, tt.want)
			}
		})
	}
}

```

Now, you could just initialize the `Author` in each test case, making it a simple and straightforward approach. But this is somewhat time-consuming and repetitive. And as soon as `Author` changes, you have to fix every test.

Instead of initializing the struct directly within the test, we can create a builder function with functional parameters.

```go
type Opts func(*Author)
func BuildAuthor(options ...opts) Author {
  // we can set some default values here
  author := &Author{ID: 1}
  for _,option := range options {
     option(author)
  }
  return *Author
}

// usage
someAuthor := BuildAuthor(func(a *Author){ a.Name="Name"})

```

With this option, we can create reusable test data and can easily call `BuildAuthor` within the test cases.

Because I found myself creating similar code to the above, I’ve created a library to assist in creating the test data.

## Introduction to the Library

The motivation behind [testcraft](https://github.com/trapajim/testcraft) was to remove some of the boilerplate needed during the test data creation and add a few little helper functions. A simple example as the above could look like this:

```go
authorFactory := testcraft.NewFactory(Author{}).Attr(func(a *Author) error {
   a.ID = 1
   a.Name="Name"
   return nil
})
someAuthor,err := authorFactory.Build()
// or use MustBuild, MustBuild panics on error
someAuthor := authorFactory.MustBuild()

```

This is basically the same code as the previously shown `BuildAuthor` function.

But there is more you can do with testcraft. Let’s look at some of the helpers. Having always the same ID is somewhat unrealistic. To get a sequence of IDs, you can create a `Sequence`:

```go
// NewSequencer can take any of the following types
// int, int32, int64, int8, float32, float64
authorSeq := NewSequencer(1)

// in the previously declared factory, we can now use
a.ID = authorSeq.Next()

```

Anytime `Build` is called, the ID will now increase by 1. The increment can be changed with `userSeq.SetIncrement(2)`.

If you just need some data and don’t care about the values, there is a `Randomize` function, which initializes the struct with random values:

```go
authorSeq := NewSequencer(1)
authorFactory := testcraft.NewFactory(Author{}).Attr(func(a *Author) error {
   a.ID = authorSeq.Next()
   a.Name="Name"
   return nil
})
// Randomize ignores the previously set values
randomAuthor, err := authorFactory.Randomize() 
// RandomizeWithAttrs will apply the previously set attributes and randomize the rest.
randomAuthor2, err := authorFactory.RandomizeWithAttrs() 

```

Next, let's have a look at how to combine factories and use the `Multiple` function:

```go
// As a reminder, here are the structs again.
type Book struct{
   ID int
   Title string
}

type Author struct {
   ID int
   Name string
   Books []Book
}
// create sequence for books
bookSeq := NewSequencer(1)
// define book factory
bookFactory := testcraft.NewFactory(Book{}).Attr(func(b *Book) error{
   b.ID = bookSeq.Next()
})
// create sequence for author
authorSeq := NewSequencer(1)
// define author factory
authorFactory := testcraft.NewFactory(Author{}).Attr(func(a *Author) error {
   a.ID = authorSeq.Next()
   // // AlphanumericBetween returns a random string of alphanumeric characters between min(3) and max(10).
   a.Name = datagen.AlphanumericBetween(3,10)
   // Multiple creates a slice of Book with 5 elements.
   a.Books = testcraft.Multiple(5, func(i int) Book {
     return bookFactory.MustRandomizeWithAttrs() 
     // functions prefixed with Must panic on error
   })
   return nil
})

```

In the above code, you also get a first glimpse of the `datagen` package as we generate an alphanumeric string between 3 and 10 characters.

I think the `datagen` package could still use some work, but for some basic work it’s good to go. 

I am still actively working on [testcraft](https://github.com/trapajim/testcraft) and add a few more features and improve the data generation package.