DEV Community

Tobias
Tobias

Posted on

Test Fixtures in Go

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:

type Book struct{
   ID int
   Title string
}

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

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

Enter fullscreen mode Exit fullscreen mode

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:

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)
            }
        })
    }
}

Enter fullscreen mode Exit fullscreen mode

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.

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"})

Enter fullscreen mode Exit fullscreen mode

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 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:

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()

Enter fullscreen mode Exit fullscreen mode

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:

// 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()

Enter fullscreen mode Exit fullscreen mode

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:

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() 

Enter fullscreen mode Exit fullscreen mode

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

// 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
})

Enter fullscreen mode Exit fullscreen mode

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 and add a few more features and improve the data generation package.

Top comments (0)