---
title: C. Table-Driven Tests
---

## What are Table-Driven Tests?

Table-driven tests are a mechanism for reducing duplicated test code and making tests more maintainable.
By grouping all related tests into a table, you consolidate dependency setup in one place, making adding
or removing dependencies easier down the line.

The simplest example of a table-driven test:
```go
func TestMultiply(t *testing.T) {
    testCases := []struct{
        name string
        x int
        y int
        expectedResult int
    }{
        {
            name: "both positive",
            x: 5,
            y: 10,
            expectedResult: 50,
        },
        {
            name: "both negative",
            x: -5,
            y: -10,
            expectedResult: 50,
        },
        {
            name: "one positive, one negative",
            x: 5,
            y: -10,
            expectedResult: -50,
        },
        {
            name: "one zero",
            x: 5,
            y: 0,
            expectedResult: 0,
        },
    }
    
    for _, tt := range testCases {
        t.Run(tt.name, func(t *testing.T) {
            result := Multiply(tt.x, tt.y)
            require.Equal(t, tt.expectedResult, result)
        })
    }
}
```
This table encapsulates all of your expected edge cases, and consolidates your function calls.
Some things to take note of:
- You only write the actual `Multiply()` function call once, but it is executed in every test.
- We use `t.Run()` to run *sub-tests* of the outer test case.
These will be represented in output as `TestMultiply/both_positive`, `TestMultiply/both_negative`, etc.
- We create an anonymous struct to represent the `testCase` struct.
This prevents leaking or overloading the package namespace with dozens of different `testCase` structs for each test.

## Table-Driven Tests and Fake Dependencies

Where table-driven tests really shine are in handling complex dependency interactions and allowing
changes to occur down the line without significant refactoring. For example, take the following method:
```go
package settings

var ErrNotFound = errors.New("not found")
var ErrAlreadyCanceled = errors.New("already canceled")

func (s *Server) CancelEvent(ctx context.Context, req *CancelEventReq) error {
    event, err := s.clients.DB.GetEvent(ctx, req.EventID)
    if err != nil {
        return nil, err
    } else if event == nil {
        return nil, ErrNotFound
    }
    
    if event.Status == db.StatusCanceled {
        return nil, ErrAlreadyCanceled
    }
    
    return s.clients.DB.CancelEvent(ctx, req.EventID)
}
```
We would write a table-driven test for the method like so:
```go
package settings_test

func TestServer_CancelEvent(t *testing.T) {
    testCases := []struct{
        name string
        getEventResp *events.Event
        getEventErr error
        cancelEventErr error
        expectedErr error
    }{
        // add test cases here
    }
    for _, tt := range testCases {
        t.Run(tt.name, func(t *testing.T) {
            s := setupTest() // this will be explained elsewhere, for now just roll with it
            
            // set up fake responses for this test instance
            s.fakeDB.GetEventReturns(tt.getEventResp, tt.getEventErr)
            s.fakeDB.CancelEventReturns(tt.cancelEventErr)
            
            err := s.server.CancelEvent(context.Background, server.CancelEventReq{EventID: "asdf"})
            require.Equal(t, tt.expectedErr, err)
        })
    }
}
```
If we were to then update our server method to add an additional dependency call:
```diff
package settings

var ErrNotFound = errors.New("not found")
var ErrAlreadyCanceled = errors.New("already canceled")
+var ErrForbidden = errors.New("forbidden")

func (s *Server) CancelEvent(ctx context.Context, req CancelEventReq) error {
    event, err := s.clients.DB.GetEvent(ctx, req.EventID)
    if err != nil {
        return err
    } else if event == nil {
        return ErrNotFound
    }     
    
    if event.Status == db.StatusCanceled {
        return ErrAlreadyCanceled
    }

+   userHasPermission, err := s.clients.Permissions.CheckPermission(ctx, req.UserID)
+   if err != nil {
+       return err
+   } else if !userHasPermission {
+       return ErrForbidden
+   }
    
    return s.clients.DB.CancelEvent(ctx, req.EventID)
}
```
We can then easily update our tests:
```diff
package settings_test

func TestServer_CancelEvent(t *testing.T) {
    testCases := []struct{
        name string
        getEventResp *events.Event
        getEventErr error
+       checkPermissionResp bool
+       checkPermissionErr error
        cancelEventErr error
        expectedErr error
    }{
        // add test cases here
    }
    for _, tt := range testCases {
        t.Run(tt.name, func(t *testing.T) {
            s := setupTest() // this will be explained elsewhere, for now just roll with it
            
            // set up fake responses for this test instance
            s.fakeDB.GetEventReturns(tt.getEventResp, tt.getEventErr)
            s.fakeDB.CancelEventReturns(tt.cancelEventErr)
+           s.fakePermissions.CheckPermissionReturns(tt.checkPermissionResp, tt.checkPermissionErr)            

            err := s.server.CancelEvent(context.Background, server.CancelEventReq{EventID: "asdf"})
            require.Equal(t, tt.expectedErr, err)
        })
    }
}
```
## Table-Driven Tests and Mutators

As your table-driven tests get more and more complex, the number of fields can become somewhat overwhelming.
It can be helpful to use a pattern that allows you to *omit* fields that are not relevant to the given test case,
implementing sane defaults for your fakes.
```go
testCases := struct{
    name string
    requestModifier func(*Request) // optional, a function that mutates the default request passed in
    eventModifier func(*Event) // optional, a function that mutates the default Event returned from the DB
    ...
}{
	...
    {
        name: "event returned is canceled",
        eventModifier = func(e *Event) { e.Status = db.Canceled }, // the only field we need to include is non-default parameter
    },
    ...
}
for _, tt := range testCases {
    t.Run(tt.name, func(t *testing.T) {
        req := defaultRequest
        if tt.requestModifier != nil {
            tt.requestModifier(req) // we apply the mutator to the request
        }
        event := defaultEvent
        if tt.eventModifier != nil {
            tt.eventModifier(event) // we apply the mutator to the event
        }
        ...
    }
}
```