Table-driven and individual subtests in Golang: which one to use
Tags: go, testing, beginners, unittest
Published: 2022-08-06 11:19:00

## Intro

Unit tests are an essential part of any code base, including the one at my workplace.  When an engineer adds a piece of new logic into the codebase, they are expected to add unit tests that will cover, at least, the most common scenarios of the new logic usage. Perhaps with a couple of edge cases as a cherry on the top.

Testing different scenarios is important, but requires writing fairly repetitive tests. Let’s look at approaches Golang offers for this.

## Testing packing of a doughnuts box
We’ll look at two different ways to write unit tests in Golang. We will use a playful example of packing a doughnuts box. The flavours of the doughnuts in the examples, are all inspired by [Crosstown doughnuts](https://www.crosstown.co.uk/). Let’s look at the code, for which we’ll write tests.

**Implementation of a doughnut box**
```Go
package doughnuts_box

import (
	"fmt"
)

type doughnutsBox struct {
	capacity  int
	doughnuts []string
}

var knownDoughnutTypes = map[string]bool{
	"Matcha Tea":                true,
	"Lime & Coconut (ve)":       true,
	"Home Made Raspberry Jam":   true,
	"Cinnamon Scroll (ve)":      true,
	"Sri Lankan Cinnamon Sugar": true,
}

func newDoughnutsBox(capacity int) *doughnutsBox {
	return &doughnutsBox{
		capacity:  capacity,
		doughnuts: make([]string, 0),
	}
}

func (b *doughnutsBox) pack(doughnuts []string) (int, error) {
	unrecognizedItems := make([]string, 0)
	var err error

	if len(doughnuts) > b.capacity {
		return 0, fmt.Errorf("failed to put %d doughnuts in the box, it's only has %d doughnuts capacity", len(doughnuts), b.capacity)
	}

	for _, doughnut := range doughnuts {
		if _, found := knownDoughnutTypes[doughnut]; found {
			b.doughnuts = append(b.doughnuts, doughnut)
			continue
		}
		unrecognizedItems = append(unrecognizedItems, doughnut)
	}

	if len(unrecognizedItems) > 0 {
		err = fmt.Errorf("the following items cannot be placed into the box: %v", unrecognizedItems)
	}
	return len(b.doughnuts), err
}
```
Now, after we familiarised ourself with the doughnut box implementation, let’s discuss how we can tests it. 

### Testing packing the doughnuts box with table-driven tests

Table-driven tests are the common way to write unit tests in Golang. You’ll find this style  everywhere.

Table tests offer a condensed way to write test scenarios while keeping code repetition to a minimum. The syntax density is coming with the drawback of poor readability. The tests may feel elegant to write, but they are often hard to read and reason about. 

The table-tests are shining in situations, where the intention is to test a large number of scenarios, with clear 
_input x produces output y_ expectations. 

For our doughnut box example, we wrote a set of scenarios in the shape of anonymous struct `testCases`. Then we run all of them as subtests with `t.Run` method, inside a familiar `for loop.`

**Implementation of doughnuts box table-driven tests**
```Go
package doughnuts_box

import (
	"testing"

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

func TestPackDoughnutsBoxTableTests(t *testing.T) {
	// Anonymous struct of test cases
	tests := []struct {
		name                           string
		boxCapacity                    int
		errorExpected                  bool
		errorMessage                   string
		items                          []string
		expectedNumOfDoughnutsInTheBox int
	}{
		{
			name:                           "Filling the box with tasty doughnuts",
			boxCapacity:                    4,
			errorExpected:                  false,
			errorMessage:                   "",
			items:                          []string{"Sri Lankan Cinnamon Sugar", "Matcha Tea", "Home Made Raspberry Jam", "Lime & Coconut (ve)"},
			expectedNumOfDoughnutsInTheBox: 4,
		},
		{
			name:                           "Attempt to fill the box with too many doughnuts",
			boxCapacity:                    4,
			errorExpected:                  true,
			errorMessage:                   "failed to put 5 doughnuts in the box, it's only has 4 doughnuts capacity",
			items:                          []string{"Sri Lankan Cinnamon Sugar", "Matcha Tea", "Home Made Raspberry Jam", "Lime & Coconut (ve)", "Lime & Coconut (ve)"},
			expectedNumOfDoughnutsInTheBox: 0,
		},
		{
			name:                           "Attempt to put a giant chocolate cookie into the box",
			boxCapacity:                    2,
			errorExpected:                  true,
			errorMessage:                   "the following items cannot be placed into the box: [Giant Chocolate Cookie]",
			items:                          []string{"Sri Lankan Cinnamon Sugar", "Giant Chocolate Cookie"},
			expectedNumOfDoughnutsInTheBox: 1,
		},
	}

	for _, tc := range tests {
		// each test case from  table above run as a subtest
		t.Run(tc.name, func(t *testing.T) {
			// Arrange
			box := newDoughnutsBox(tc.boxCapacity)

			// Act
			numOfDoughnutsInTheBox, err := box.pack(tc.items)

			// Assert
			if tc.errorExpected {
				require.Error(t, err)
				assert.Equal(t, tc.errorMessage, err.Error())
			}
			assert.Equal(t, tc.expectedNumOfDoughnutsInTheBox, numOfDoughnutsInTheBox)
		})
	}
}
```
We looked at the "happy path" test case and two test cases for errors we expect to be common - trying to put too many doughnuts into a box and an attempt to put something that isn't doughnut into the box.

Next, we'll look at how we can re-write the same tests in different style.

### Testing packing the doughnuts box with individual subtests

Writing tests scenarios as an individual subtests is less common in Golang. This fact is regretful, in my opinion.  But still there are enough of examples of this syntax.

Individual subtests syntax, unlike table tests, favours readability overall “code dryness”. The tests may look more repetitive, but each individual subtest represents an individual “story”. As such, it is easier to follow behaviour-driven testing principles with individual subtests than with table-driven tests.

In our small example, all arrangements are made within the individual subtests.  In more complex examples, the state shared between subtests can be initialised in the body of the parent test. And repetitive setup/teardown can be extracted into helper functions.

**Implementation of Donuts box test as individual subtests**
```Go
package doughnuts_box

import (
	"testing"

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

func TestPackDoughnutsBoxSubtests(t *testing.T) {
	t.Run("It fills the box with tasty doughnuts", func(t *testing.T) {
		// Arrange
		items := []string{"Sri Lankan Cinnamon Sugar", "Matcha Tea", "Home Made Raspberry Jam", "Lime & Coconut (ve)"}
		box := newDoughnutsBox(4)

		// Act
		numOfDoughnutsInTheBox, err := box.pack(items)

		// Assert
		require.NoError(t, err)
		assert.Equal(t, numOfDoughnutsInTheBox, 4)
	})
	t.Run("It fails to fill the box with too many doughnuts", func(t *testing.T) {
		// Arrange
		items := []string{"Sri Lankan Cinnamon Sugar", "Matcha Tea", "Home Made Raspberry Jam", "Lime & Coconut (ve)", "Lime & Coconut (ve)"}
		box := newDoughnutsBox(4)

		// Act
		numOfDoughnutsInTheBox, err := box.pack(items)

		// Assert
		require.Error(t, err)
		assert.Equal(t, "failed to put 5 doughnuts in the box, it's only has 4 doughnuts capacity", err.Error())
		assert.Equal(t, 0, numOfDoughnutsInTheBox)
	})
	t.Run("It fails to put a giant chocolate cookie into the box", func(t *testing.T) {
		// Arrange
		items := []string{"Sri Lankan Cinnamon Sugar", "Giant Chocolate Cookie"}
		box := newDoughnutsBox(4)

		// Act
		numOfDoughnutsInTheBox, err := box.pack(items)

		// Assert
		require.Error(t, err)
		assert.Equal(t, "the following items cannot be placed into the box: [Giant Chocolate Cookie]", err.Error())
		assert.Equal(t, 1, numOfDoughnutsInTheBox)
	})
}
```
We wrote the same three test cases - happy path and two common error scenarios as individual sub-tests. In our example, they even took approximately the same number of code lines. 58 lines for individual subtests, comparing to 63 lines of table-driven tests example.

## Conclusions

Now, when examples for two styles for unit tests are presented, it is time to decide which one to use and when. 

As an engineer, you should exercise your better judgement on how to write unit tests and what syntax suits best. You can use the following little diagram to help you to decide, which syntax to use.

![Diagram helper to choice between table-driven and individual sub-test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zhvnfbxuajp6zcolc6q5.png)

I hope the diagram helps. If you have a strong opinion for one syntax over another or have your own way of writing Golang unit tests, please share in the comments!